Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e57811b
main add is simba_hinted_contract.py, which provides a type hinted ve…
bpbirch May 26, 2021
53ef199
adding simba_hinted_contract.py
bpbirch May 26, 2021
7bb1701
removed some minor comments
bpbirch May 26, 2021
72386ab
removed slight typo
bpbirch May 26, 2021
b6cc161
removed outdated method from SimbaHintedContract
bpbirch May 26, 2021
e5654f9
some commenting
bpbirch May 26, 2021
5b7fb0e
minor comment change
bpbirch May 27, 2021
f7973f0
removed pprint import, converted methods to snake case'
bpbirch May 27, 2021
519abe2
removed some comments / cells
bpbirch May 27, 2021
8c4c494
fixed async -> async_method in simba_contract.py. Allowed all submit_…
bpbirch May 27, 2021
9e60574
structs are now included as classes. type hinting includes custom cla…
bpbirch May 28, 2021
802746d
All structs are now included as classes in our contract class. Type h…
bpbirch May 28, 2021
608947a
moved class -> dict converter methods to a base class that gets inher…
bpbirch May 28, 2021
7c10e3c
conversion of class instances to dictionaries for API calls is now do…
bpbirch May 29, 2021
2bd487a
minor typo fix
bpbirch May 30, 2021
a666e4e
added some type hinting
bpbirch May 30, 2021
a22622f
added line for SimbaHintedContract to use get request for metadata. C…
bpbirch May 30, 2021
6dd5672
comment fix
bpbirch May 30, 2021
c2818aa
comments added regarding how contract_name, metadata should be assign…
bpbirch May 30, 2021
ca07cab
changed some variable names to camel case
bpbirch May 30, 2021
5d0ff48
fixed .get_metadata in simba_contract, simba_hinted_contract, and par…
bpbirch May 31, 2021
7b32919
changed simba_hinted_contract interface to get metadata through a get…
bpbirch May 31, 2021
1ae7b83
added unit tests for param_checking_contract functionality, contained…
bpbirch May 31, 2021
31bceac
removed unnecessary files
bpbirch May 31, 2021
39bb725
removed redundant code in simba_hinted_contract. Added docstrings to …
bpbirch Jun 3, 2021
81c291b
moved test files / examples into more logical locations
bpbirch Jun 3, 2021
a58bf89
removed token
bpbirch Jun 3, 2021
aff3861
added notebooks
bpbirch Jun 4, 2021
991391a
moved example runs into examples.ipynb. Modified simba_hinted_contrac…
bpbirch Jun 7, 2021
83a37b9
got rid of redundant notebook
bpbirch Jun 7, 2021
8283509
write_contract is now automatically called when SimbaHintedContract o…
bpbirch Jun 7, 2021
7ea27b9
jinja template is now accessed using importlib.resources.
bpbirch Jun 7, 2021
3073210
removed unit tests from /tests
bpbirch Jun 8, 2021
1606452
fixed typo in examples.ipynb
bpbirch Jun 8, 2021
447cf2a
removed some unused variables and methods
bpbirch Jun 8, 2021
ed2c064
removed an unnecessary import statement
bpbirch Jun 9, 2021
e90ab06
added funcionality to pass a customized class_contract_name, instead …
bpbirch Jun 9, 2021
a01bbab
fixed typo in contract.tpl
bpbirch Jun 9, 2021
80f28c7
added logic in libsimba.file_handler to aid developer in passing corr…
bpbirch Jun 11, 2021
5d4496d
added json.dumps code to submit_contract_method_with_files in simba_c…
bpbirch Jun 12, 2021
050db61
added logic to pass non-stringified data to request in submit_contrac…
bpbirch Jun 12, 2021
a25482d
modified simba_contract.py so submit_contract_method_with_file and su…
bpbirch Jun 12, 2021
35ee419
notebook is updated and ready to go
bpbirch Jun 12, 2021
6bdf7cb
removed some print statements from file_handler.py
bpbirch Jun 12, 2021
c49dccd
removed some unnecessary files in /tests/data
bpbirch Jun 12, 2021
aa730c8
modified file_handler.py and simba_hinted_contract.py to allow for us…
bpbirch Jun 14, 2021
dea56fa
added default behavior to open_files in file_handler.py
bpbirch Jun 14, 2021
e0286a8
updated output class in notebook
bpbirch Jun 14, 2021
927ce9d
fixed array length checking so that we're accounting for reverse dime…
bpbirch Jun 18, 2021
2882f05
resolve poetry.lock conflict
abrinckm Dec 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions libsimba/class_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Any
import json

def convert_classes(inputs):
"""
solidity structs represented as classes will have "class_to_dict_converter" attr
this function gets called in contract method calls inside simba_contract.py

Args:
inputs (dict): dict of {param_name: param_value} form
"""
for attr_name, attr_value in inputs.items():
if hasattr(attr_value, "class_to_dict_converter"):
attr_value.class_to_dict_converter()
inputs[attr_name] = attr_value.__dict__


class ClassToDictConverter:
def class_to_dict_converter_helper(self, class_dict:dict, attr_name:str, attr_value:Any):
"""
Recursively convert method inputs from class instances to dicts
We call this method because when we pass a struct in the API, it expects a dictionary, not a class instance

Args:
class_dict (dict): dict formatted version of a class, obtained from classInstance.__dict__
attr_name (str)): attribute name in our inputs dictionary
attr_value (Any): value of attribute in inputs dictionary. if this is a class instance with attribute __dict__, then we convert to dict
"""
if hasattr(attr_value, '__dict__'):
class_dict[attr_name] = attr_value.__dict__
for att_name, att_val in class_dict[attr_name].items():
self.class_to_dict_converter_helper(class_dict[attr_name], att_name, att_val)

def class_to_dict_converter(self):
for att_name, att_value in self.__dict__.items():
self.class_to_dict_converter_helper(self.__dict__, att_name, att_value)

56 changes: 56 additions & 0 deletions libsimba/file_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Any, Tuple, List

from requests.api import get

def get_file_for_upload(file_name:str, file_object_or_path:Any, read_mode: str = 'rb') -> tuple:
"""
takes file_name and file_object_or_path and formats files in a way that is acceptable to Python
requests for multipart encoded file.
if type(file_object_or_path) == str, then we convert to a readable object

Args:
file_name (str): file name to assign to file_object_or_path
file_object_or_path (Any): should be either a file path or a readable file object
read_mode (str, optional): only relevant if as_path == True. read mode for opening file (eg 'rb', 'r'). Defaults to 'rb'.

Returns:
tuple: multipart form encoded file format: ('file', (file_name, readable_file_object))
"""
if type(file_object_or_path) == str:
return ('file', (file_name, open(file_object_or_path, read_mode)))
else:
return ('file', (file_name, file_object_or_path))

def open_files(files: List[Tuple]) -> List[tuple]:
"""
Takes a list of form [(file_name, file_path_or_object, 'r'),...], and returns a list of tuples in correct format for multipart encoded file
Note that 'r' here is the read mode in which we want to read our file
open_files is written so that if a user does not pass a read_mode in any of their tuples in Files, then 'r' is the default read_mode

Args:
files (List[Tuple]): list of form [(file_name, file_path_or_object),...]
read_mode (str, optional): to read bytes objects, should be 'rb', to read text, should be 'r', etc. Defaults to 'rb'.

Returns:
List[tuple]: list in form [('file', (file_name, readable_file_object)),...]
"""
fileList = []
for fileTuple in files:
if len(fileTuple) == 3:
file_name, file_object_or_path, read_mode = fileTuple
fileList.append(get_file_for_upload(file_name, file_object_or_path, read_mode=read_mode))
elif len(fileTuple) == 2:
file_name, file_object_or_path = fileTuple
read_mode = 'r'
fileList.append(get_file_for_upload(file_name, file_object_or_path, read_mode=read_mode))
return fileList

def close_files(files:List[tuple]):
"""
closes files after we call open_files

Args:
files (List[tuple]): list in form [('file', (file_name, readable_file_object)),...]
"""
for _, (file_name, file_object) in files:
file_object.close()
230 changes: 230 additions & 0 deletions libsimba/param_checking_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
from typing import List, Optional, Any, Dict
from libsimba.decorators import auth_required
from libsimba.utils import build_url
import requests
import json

class ParamCheckingContract:
def __init__(self, base_api_url, app_name, contract_name):
self.app_name = app_name
self.contract_name = contract_name
self.base_api_url = base_api_url
self.contract_uri = "{}/contract/{}".format(self.app_name, self.contract_name)
self.async_contract_uri = "{}/async/contract/{}".format(self.app_name, self.contract_name)
self.metadata = self.get_metatadata()
# we call param_restrictions right away, so we have a dictionary of restrictions to reference for methods
self.params_restricted = self.param_restrictions()

@auth_required
def get_metadata(self, headers, opts: Optional[dict] = None):
opts = opts or {}
url = build_url(self.base_api_url, "v2/apps/{}/?format=json".format(self.contract_uri), opts)
resp = requests.get(url, headers=headers)
metadata = resp.json()
return metadata

def is_array(self, param) -> bool:
return param.endswith(']')

def array_restrictions(self, arr:str) -> dict:
"""
returns a dictionary that maps dimension to array-length restriction
Our outer-most array is associated with our lowest dimension: 0
Since solidity array dimensions are backward (uint256[][4] is 4 dynamically lengthed arrays),
we have to reverse our array item first

for example, 'uint256[5][3][][4]' would get mapped to: {0: '4', 1: None, 2: '3', 3: '5'}

Args:
arr (str): string formatted solidity style array, eg 'uint256[5][3][][4]'

Returns:
[dict]: dict mapping of dimension -> array-length, eg {0: '4', 1: None, 2: '3', 3: '5'}
"""
print('arr:', arr)
reverseArray = ''
for ch in arr[::-1]:
if ch == '[':
reverseArray += ']'
elif ch == ']':
reverseArray += '['
else:
reverseArray += ch
print('reverseArray:', reverseArray)
arr_lengths = {}
for i in range(self.get_dimensions(arr)):
arr_len = reverseArray[reverseArray.find('[')+1:reverseArray.find(']')]
arr_lengths[i] = int(arr_len) if arr_len else None
reverseArray = reverseArray[reverseArray.find(']')+1:]
print('arr_lengths:', arr_lengths)
return arr_lengths

def get_dimensions(self, param:str, dims:Optional[int] = 0) -> int:
"""
Recursive function to determine dimensions of array type

Args:
param (str): string formatted parameter (eg 'str[][]' will return 2)
dims (Optional[int], optional): [description]. Defaults to 0.

Returns:
[int]: number of dimensions in array
"""
if '[' not in param:
return dims
param = param[param.find('[')+1:]
dims += 1
return self.get_dimensions(param, dims)

def param_restrictions(self) -> dict:
"""
This will return a dictionary of methods that have either array parameters with length restrictions,
or uint parameters. This includes methods that have dynamic (non-length restricted) parameters for
which the elements are uints.

If a method does NOT have one of the following:
fixed-length (length-restricted) array parameters
uint parameters
fixed-length or dynamic array parameters whose elements are uints or uint-elemented arrays

then the method WILL NOT be included in our return array.
This will allow for a quick check when we call each method to ask whether we need to check
param restrictions

Note that if a method does not have any params with array length restrictions, then the key
'array_params' will not be populated for that method in our return object
Similarly, if a method does not have any params that are uints, then the key 'uint_params'
will not be populated for that method in our return object

Returns:
[dict]:
example return for a contract with methods 'an_arr', 'array_params', and 'bbb':

{'an_arr': {'array_params': {'first': {0: None, 'contains_uint': True}}},
'another_uint_param': {'uint_params': ['another_uint', 'second_uint']},
'bbb': {'array_params': {'first': {0: None, 1: None, 'contains_uint': True}}}
}
"""
md = self.metadata
contract = md['contract']
paramRestrictions = {}
methods = {method: values for method, values in contract['methods'].items()}
for method, params in methods.items():
for paramDict in params['params']:
paramName = paramDict['name']
rawType = paramDict['type']
contains_or_is_uint = rawType.startswith('uint')
# don't do anything if not an array and not contains_or_is_uint
# we're only worried about uint type checking and array length checking
if not contains_or_is_uint and not self.is_array(paramName):
continue
if method not in paramRestrictions:
paramRestrictions[method] = {}
if contains_or_is_uint and not self.is_array(rawType):
# we are just keeping a list of paramNames for params that are uint_params
if 'uint_params' not in paramRestrictions[method]:
paramRestrictions[method]['uint_params'] = [paramName]
else:
paramRestrictions[method]['uint_params'].append(paramName)
elif self.is_array(rawType):
if 'array_params' not in paramRestrictions[method]:
paramRestrictions[method]['array_params'] = {}
arrRestrictions = self.array_restrictions(rawType)
arrRestrictions['contains_uint'] = contains_or_is_uint
paramRestrictions[method]['array_params'][paramName] = arrRestrictions
return paramRestrictions

def check_array_restrictions(self, arr:List[Any], param_name, param_restrictions_dict:dict, level=0):
"""
recursively checks lengths of arrays and sub-arrays for parameter
if element of an array is not an array, and there are uint restrictions for the parameter,
then we check to make sure that the element is an int >= 0

we also compare elements in each array to make sure there is no element mixing

Args:
arr (List[Any]): list to have lengths and elements checked
param_name ([type]): parameter name - required to look up array dimension requirements
param_restrictions_dict (Dict): restriction dict derived from a particular method's key in self.param_restrictions() call
eg {'first': {0: None, 'contains_uint': True}}
level (int, optional): recursively increases for increasing dimensions. Defaults to 0.

Raises:
ValueError: [description]
TypeError: [description]
TypeError: [description]
ValueError: [description]

Returns:
[bool]: True if no exceptions raised
"""
level_restriction = param_restrictions_dict[param_name].get(level, "Too Many Dimensions")
if level_restriction == "Too Many Dimensions":
raise ValueError("check_array_restrictions: passed array contains too many dimensions")
if level_restriction is not None:
if len(arr) != level_restriction:
raise ValueError("check_array_restrictions: array length error")
level += 1
for i, element in enumerate(arr):
# first check to make sure we don't have any type mixing in arrays
if i > 0 and type(arr[i]) != type(arr[i-1]):
raise TypeError("check_array_restrictions: array element types do not match")
# then recursively check each subarray
if type(element) == list:
self.check_array_restrictions(element, param_name, param_restrictions_dict, level=level)
else:
if param_restrictions_dict[param_name]['contains_uint'] is True:
if type(element) != int:
raise TypeError("check_array_restrictions: array elements must be type int")
if element < 0:
raise ValueError("check_array_restrictions: array elements must be non-negative")
return True

def check_uint_restriction(self, param_value:int):
"""
check that parameter value is int >= 0

Args:
param_value (int): [description]

Raises:
ValueError: [description]
ValueError: [description]

Returns:
[bool]: True if no exceptions are raised
"""
if param_value < 0:
raise ValueError("parameter value must be >= 0")
if type(param_value) != int:
raise ValueError("parameter must be type int")
return True

def validate_params(self, method_name:str, inputs:dict):
"""
should be called at any method call that requires inputs
calls check_uint_restrictions and check_array_restrictions

Args:
method_name (str): method name for which we are checking validating parameters
inputs (dict): dict of inputs of {'param_name_string': param_value} form

Returns:
[bool]: True if no exceptions are raised
"""
paramRestrictions = self.params_restricted
method_restrictions = paramRestrictions.get(method_name, None)
if not method_restrictions:
# this means the method had no array length or uint restrictions
return True
uint_params = paramRestrictions.get(f'{method_name}', {}).get('uint_params', {})
array_params = paramRestrictions.get(f'{method_name}', {}).get('array_params', {})


for param_name, param_value in inputs.items():
if param_name in uint_params:
self.check_uint_restriction(param_value)
if param_name in array_params:
self.check_array_restrictions(param_value, param_name, array_params)
return True

Loading