Skip to content
This repository has been archived by the owner on Sep 21, 2022. It is now read-only.

Commit

Permalink
Merge pull request #9 from blockvigil/support-contract-imports
Browse files Browse the repository at this point in the history
Support contract imports
  • Loading branch information
anomit authored Jun 10, 2020
2 parents ac66533 + 8d54328 commit 481bed0
Show file tree
Hide file tree
Showing 11 changed files with 673 additions and 288 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ __pycache__
ev_cli.egg-info
settings.json
*.spec
.pytest_cache
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ install: pip3 install -r requirements.txt --upgrade pip # all three OSes agree

script:
- python3 --version || python --version;
- pytest
- pytest -spPf test_cli.py
- pyinstaller --onefile click_cli.py --hidden-import click --clean --name="ev-cli"
- mkdir ethvigil-cli
- mv dist/ev-cli ethvigil-cli/
Expand Down
29 changes: 11 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,27 +114,18 @@ REST API prefix: http://localhost:9000/api/v0.1
Contracts deployed/verified:
=============
Name: FixedSupplyToken
Address: 0xb8254a02fa7da599053006913bbed0a13fa0385f
--------------------
Name: FixedSupplyToken
Address: 0x7b5622290fd8fb7b89707a7dc68201d5eb833507
--------------------
Name: SignerControlBase
Address: 0x746254cb1888a0f073fca2cf397457fb3e54396f
--------------------
Name: FixedSupplyToken
Address: 0xbbd8cda5503e1df2983f738ad131a2304528e3dd
--------------------
Name: Ballot
Address: 0x616cc6fd735462df69fc0f5bdb61bc12921b3b17
Name: ERC20Mintable
Address: 0xaec35285e21045bd4f159165015cc1f9df14c13e
--------------------
```

## Deploy a Solidity smart contract

We have included a couple of smart contracts written in Solidity in the code repo to help you test out their deployment right away.
You can find them in `token.sol` and `SignerControlBase.sol`
You can find them under `contracts/` as `ERC20Mintable.sol` and `SignerControlBase.sol`

The syntax to deploy a contract through the CLI tool is:

Expand All @@ -143,15 +134,17 @@ ev-cli deploy <path-to-solidity-contract> \
--contractName=<contract-name> \
--constructorInputs='JSON representation of the constructor arguments'
```
>Currently EthVigil API accepts **only one** Solidity file that contains the entire smart contract logic. It will be enhanced in the near future where we will allow multiple files to be uploaded to ease development with imports of other modules and libraries
>Currently EthVigil API accepts Solidity files that import other Solidity files containing smart contracts and library code, **within the same directory**. For example, your imports must be of the form `import './SafeMath.sol'` denoting that `SafeMath.sol` is to be found in the same directory.
>We will soon add support for parsing relative import paths as well. Feel free to create a pull request against our [Github repo](https://github,com/blockvigil/ethvigil-cli) or chat with us on the [public discord channel]() if you wish to contribute to solving this.
### ERC20 token contract example - token.sol
### ERC20 token contract example - ERC20Mintable.sol
```bash
ev-cli deploy token.sol --contractName=FixedSupplyToken
ev-cli deploy contracts/ERC20Mintable.sol --contractName=ERC20Mintable --constructorInputs='["TestTokenName", "SYMB", 18]'

Contract FixedSupplyToken deployed successfully
Contract Address: 0xb8254a02fa7da599053006913bbed0a13fa0385f
Deploying tx: 0xd5318cf2bc163e267fecbb85ad2688f088fb2f45bc93baa1d9530f2d23b64a26
Contract ERC20Mintable deployed successfully
Contract Address: 0xaec35285e21045bd4f159165015cc1f9df14c13e
Deploying tx: 0x17a8009565731f45a1621905a7e85e84a6330b485ac3e7e450d90f126b6c3006
```
Observe that `--constructorInputs` has been left empty. It is optional for contracts that have no constructor inputs programmed.

Expand Down
136 changes: 95 additions & 41 deletions click_cli.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
import sys
import click
import requests
from eth_account.messages import defunct_hash_message
from eth_account.messages import defunct_hash_message, encode_defunct
from eth_account.account import Account
import getpass
import json
import os
import pwd
from eth_utils import to_normalized_address
from solidity_parser import parser

CONTEXT_SETTINGS = dict(
help_option_names=['-h', '--help']
)

CLEAN_SLATE_SETTINGS = {
"PRIVATEKEY": None,
"INTERNAL_API_ENDPOINT": "https://beta.ethvigil.com/api",
"INTERNAL_API_ENDPOINT": "https://beta.ethvigil.com/api" if not "ETHVIGIL_API_ENDPOINT" in os.environ else os.environ['ETHVIGIL_API_ENDPOINT'],
"REST_API_ENDPOINT": None,
"ETHVIGIL_USER_ADDRESS": "",
"ETHVIGIL_API_KEY": ""

}

if "ETHVIGIL_CLI_TESTMODE" in os.environ:
settings_json_loc = os.getcwd() + '/.ethvigil/settings.json'
settings_json_parent_dir = os.getcwd() + '/.ethvigil'
else:
settings_json_loc = pwd.getpwuid(os.getuid()).pw_dir + '/.ethvigil/settings.json'
settings_json_parent_dir = pwd.getpwuid(os.getuid()).pw_dir + '/.ethvigil'

@click.group(context_settings=CONTEXT_SETTINGS)
@click.pass_context
def cli(ctx):
try:
with open(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil/settings.json', 'r') as f:
with open(settings_json_loc, 'r') as f:
s = json.load(f)
except:
# settings file does not exist, copy over settings.null.json
try:
os.stat(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil')
os.stat(settings_json_parent_dir)
except:
os.mkdir(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil')
os.mkdir(settings_json_parent_dir)
# create settings file from empty JSON file
with open(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil/settings.json', 'w') as f2:
with open(settings_json_loc, 'w') as f2:
json.dump(obj=CLEAN_SLATE_SETTINGS, fp=f2)
s = CLEAN_SLATE_SETTINGS
finally:
Expand All @@ -43,8 +52,8 @@ def cli(ctx):

def ev_login(internal_api_endpoint, private_key, verbose=False):
msg = "Trying to login"
message_hash = defunct_hash_message(text=msg)
signed_msg = Account.signHash(message_hash, private_key)
message_hash = encode_defunct(text=msg)
signed_msg = Account.sign_message(message_hash, private_key)
# --THUNDERVIGIL API CALL---
headers = {'accept': 'application/json', 'Content-Type': 'application/json'}
r = requests.post(internal_api_endpoint + '/login',
Expand All @@ -58,10 +67,10 @@ def ev_login(internal_api_endpoint, private_key, verbose=False):
return None


def ev_signup(internal_api_endpoint, invite_code, private_key):
def ev_signup(internal_api_endpoint, invite_code, private_key, verbose):
msg = "Trying to signup"
message_hash = defunct_hash_message(text=msg)
signed_msg = Account.signHash(message_hash, private_key)
message_hash = encode_defunct(text=msg)
signed_msg = Account.sign_message(message_hash, private_key)
# --ETHVIGIL API CALL to /signup---
try:
r = requests.post(internal_api_endpoint + '/signup', json={
Expand All @@ -70,16 +79,17 @@ def ev_signup(internal_api_endpoint, invite_code, private_key):
except:
return False
else:
print(r.url)
print(r.text)
if verbose:
print(r.url)
print(r.text)
if r.status_code == requests.codes.ok:
return r.json()
else:
return False


def fill_rest_api_endpoint(new_endpoint):
with open(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil/settings.json', 'w') as f:
with open(settings_json_loc, 'w') as f:
j = json.load(f)
if 'REST_API_ENDPOINT' not in j or j['REST_API_ENDPOINT'] != new_endpoint:
j['REST_API_ENDPOINT'] = new_endpoint
Expand All @@ -92,45 +102,56 @@ def fill_rest_api_endpoint(new_endpoint):
@click.pass_obj
def init(ctx_obj, verbose):
if not ctx_obj['settings']['PRIVATEKEY']:
# click.echo('Choosing EthVigil Alpha instance https://alpha.ethvigil.com/api ...')
invite_code = click.prompt('Enter your invite code')
invite_code = click.prompt('Enter your invite code', hide_input=True)
new_account = Account.create('RANDOM ENTROPY WILL SUCK YOUR SOUL')
signup_status = ev_signup(ctx_obj['settings']['INTERNAL_API_ENDPOINT'], invite_code, new_account.key.hex())
signup_status = ev_signup(ctx_obj['settings']['INTERNAL_API_ENDPOINT'], invite_code, new_account.key.hex(), verbose)
if not signup_status:
click.echo('Signup failed')
return
else:
ctx_obj['settings']['PRIVATEKEY'] = new_account.key.hex()
ctx_obj['settings']['ETHVIGIL_USER_ADDRESS'] = new_account.address
with open(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil/settings.json', 'w') as f:

with open(settings_json_loc, 'w') as f:
json.dump(ctx_obj['settings'], f)
click.echo('Sign up succeeded...')
click.echo('Logging in with your credentials...')
login_data = ev_login(ctx_obj['settings']['INTERNAL_API_ENDPOINT'], new_account.key.hex(), verbose)
if login_data:
if len(login_data.keys()) > 0:
ctx_obj['settings']['ETHVIGIL_API_KEY'] = login_data['key']
ctx_obj['settings']['READ_API_KEY'] = login_data['readKey']
ctx_obj['settings']['REST_API_ENDPOINT'] = login_data['api_prefix']
with open(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil/settings.json', 'w') as f:
json.dump(ctx_obj['settings'], f)

click.echo('You have signed up and logged in successfully to EthVigil Alpha')
click.echo('---YOU MIGHT WANT TO COPY THESE DETAILS TO A SEPARATE FILE---')
click.echo('===Private key (that signs messages to interact with EthVigil APIs===')
click.echo(ctx_obj['settings']['PRIVATEKEY'])
click.echo('===ETHEREUM hexadecimal address corresponding to above private key===')
click.echo(ctx_obj['settings']['ETHVIGIL_USER_ADDRESS'])
if verbose:
click.echo('---YOU MIGHT WANT TO COPY THESE DETAILS TO A SEPARATE FILE---')
click.echo('===Private key (that signs messages to interact with EthVigil APIs===')
click.echo(ctx_obj['settings']['PRIVATEKEY'])
click.echo('===ETHEREUM hexadecimal address corresponding to above private key===')
click.echo(ctx_obj['settings']['ETHVIGIL_USER_ADDRESS'])
with open(settings_json_loc, 'w') as f:
json.dump(ctx_obj['settings'], f)
if verbose:
click.echo('Wrote context object to settings location')
click.echo(settings_json_loc)
click.echo('Context object')
click.echo(ctx_obj)
sys.exit(0)
else:
click.echo('Login failed with credentials. Run `ev-cli reset`.')
sys.exit(2)
else:
click.echo("A registered private key exists for this ev-cli installation. Run ev-cli reset if you wish"
" to do a fresh install")
sys.exit(1)


@cli.command()
@click.pass_obj
def reset(ctx_obj):
if click.confirm('Do you want to reset the current EthVigil CLI configuration and state?'):
try:
with open(pwd.getpwuid(os.getuid()).pw_dir+'/.ethvigil/settings.json', 'w') as f2:
with open(settings_json_loc, 'w') as f2:
json.dump(CLEAN_SLATE_SETTINGS, f2)
finally:
click.echo('EthVigil CLI tool has been reset. Run `ev-cli init` to reconfigure.')
Expand All @@ -143,10 +164,11 @@ def login(ctx_obj, verbose_flag):
if not ctx_obj['settings']['PRIVATEKEY']:
click.echo('No Private Key configured in settings.json to interact with EthVigil APIs. Run `ev-cli init`.')
return
click.echo(ctx_obj)
account_data = ev_login(internal_api_endpoint=ctx_obj['settings']['INTERNAL_API_ENDPOINT'],
private_key=ctx_obj['settings']['PRIVATEKEY'],
verbose=verbose_flag)
fill_rest_api_endpoint(ctx_obj, account_data['api_prefix'])
fill_rest_api_endpoint(account_data['api_prefix'])


@cli.command()
Expand Down Expand Up @@ -205,7 +227,7 @@ def importsettings(importfile, verbose):
'for constructor inputs of type (string, address). '
'Can be left empty if there are no inputs accepted by the constructor')
@click.option('--verbose', 'verbose', type=bool, default=False)
@click.argument('contract', type=click.File('r'))
@click.argument('contract', type=click.Path(exists=True, dir_okay=False))
@click.pass_obj
def deploy(ctx_obj, contract_name, inputs, verbose, contract):
"""
Expand All @@ -223,23 +245,55 @@ def deploy(ctx_obj, contract_name, inputs, verbose, contract):
c_inputs = json.loads(inputs)
else:
c_inputs = list() # an empty list
sources = dict()
if contract[0] == '~':
contract_full_path = os.path.expanduser(contract)
else:
contract_full_path = contract
resident_directory = ''.join(map(lambda x: x+'/', contract_full_path.split('/')[:-1]))
contract_file_name = contract_full_path.split('/')[-1]
contract_file_obj = open(file=contract_full_path)

main_contract_src = ''
while True:
chunk = contract.read(1024)
chunk = contract_file_obj.read(1024)
if not chunk:
break
contract_src += chunk

main_contract_src += chunk
sources[f'ev-cli/{contract_file_name}'] = {'content': main_contract_src}
# loop through imports and add them to sources
source_unit = parser.parse(main_contract_src)
source_unit_obj = parser.objectify(source_unit)

for each in source_unit_obj.imports:
import_location = each['path'].replace("'", "")
# TODO: follow specified relative paths and import such files too
if import_location[:2] != './':
click.echo(f'You can only import files from within the same directory as of now', err=True)
return
# otherwise read the file into the contents mapping
full_path = resident_directory + import_location[2:]
imported_contract_obj = open(full_path, 'r')
contract_src = ''
while True:
chunk = imported_contract_obj.read(1024)
if not chunk:
break
contract_src += chunk
sources[f'ev-cli/{import_location[2:]}'] = {'content': contract_src}
msg = "Trying to deploy"
message_hash = defunct_hash_message(text=msg)
message_hash = encode_defunct(text=msg)
# deploy from alpha account
signed_msg = Account.signHash(message_hash, ctx_obj['settings']['PRIVATEKEY'])
signed_msg = Account.sign_message(message_hash, ctx_obj['settings']['PRIVATEKEY'])
deploy_json = {
'msg': msg,
'sig': signed_msg.signature.hex(),
'name': contract_name,
'inputs': c_inputs,
'code': contract_src
'sources': sources,
'sourceFile': f'ev-cli/{contract_file_name}'
}
# click.echo(deploy_json)
# --ETHVIGIL API CALL---
r = requests.post(ctx_obj['settings']['INTERNAL_API_ENDPOINT'] + '/deploy', json=deploy_json)
if verbose:
Expand All @@ -262,8 +316,8 @@ def registerhook(ctx_obj, contract, url):
headers = {'accept': 'application/json', 'Content-Type': 'application/json',
'X-API-KEY': ctx_obj['settings']['ETHVIGIL_API_KEY']}
msg = 'dummystring'
message_hash = defunct_hash_message(text=msg)
sig_msg = Account.signHash(message_hash, ctx_obj['settings']['PRIVATEKEY'])
message_hash = encode_defunct(text=msg)
sig_msg = Account.sign_message(message_hash, ctx_obj['settings']['PRIVATEKEY'])
method_args = {
"msg": msg,
"sig": sig_msg.signature.hex(),
Expand Down Expand Up @@ -293,8 +347,8 @@ def registerhook(ctx_obj, contract, url):
@click.pass_obj
def addhooktoevent(ctx_obj, contractaddress, hookid, events):
msg = 'dummystring'
message_hash = defunct_hash_message(text=msg)
sig_msg = Account.signHash(message_hash, ctx_obj['settings']['PRIVATEKEY'])
message_hash = encode_defunct(text=msg)
sig_msg = Account.sign_message(message_hash, ctx_obj['settings']['PRIVATEKEY'])
events_to_be_registered_on = list()
if not events:
events_to_be_registered_on.append('*')
Expand Down Expand Up @@ -335,8 +389,8 @@ def addhooktoevent(ctx_obj, contractaddress, hookid, events):
def enabletxmonitor(ctx_obj, contractaddress, hookid):
# enable tx monitoring on contract
msg = 'dummystring'
message_hash = defunct_hash_message(text=msg)
sig_msg = Account.signHash(message_hash, ctx_obj['settings']['PRIVATEKEY'])
message_hash = encode_defunct(text=msg)
sig_msg = Account.sign_message(message_hash, ctx_obj['settings']['PRIVATEKEY'])
method_args = {
"msg": msg,
"sig": sig_msg.signature.hex(),
Expand Down
Loading

0 comments on commit 481bed0

Please sign in to comment.