Skip to content
This repository has been archived by the owner on Dec 1, 2023. It is now read-only.

Added support for signer #33

Merged
merged 17 commits into from
Dec 5, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ A few things to notice here:
3. The `--alias` parameter lets me create an unique identifier for future interactions, if no alias is set then the contract's address can be used as identifier
4. By default Nile works on local, but you can pass `--network mainnet` to deploy directly to a public chain! Notice that `mainnet` refers to StarkNet main chain, that's settled on Goerli testnet of Ethereum ([mainnet deployment this month!](https://medium.com/starkware/starknet-alpha-is-coming-to-mainnet-b825829eaf32))

### `setup`
```sh
nile setup PKEY

🚀 Deploying Account
🌕 artifacts/Account.json successfully deployed to 0x07db6b52c8ab888183277bc6411c400136fe566c0eebfb96fffa559b2e60e794
📦 Registering deployment as account-0 in localhost.deployments.txt
Invoke transaction was sent.
Contract address: 0x07db6b52c8ab888183277bc6411c400136fe566c0eebfb96fffa559b2e60e794
Transaction hash: 0x17
```

A few things to notice here:

1. `nile set <env_var>` looks for an environement variable with the same name whose value is a private key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no nile set command :)

2. This created a `localhost.accounts.json` file storing all data related to accounts management

### `proxy`
Execute a transaction through the `Account` associated with the private key used.
```sh
nile proxy <env_signer> <contract_address> <contract_method> <args>

Invoke transaction was sent.
Contract address: 0x07db6b52c8ab888183277bc6411c400136fe566c0eebfb96fffa559b2e60e794
Transaction hash: 0x1c
```

### `send`
Acts like `proxy` with the exception you can use it like you would use `nile invoke`.
Execute a transaction through the `Account` associated with the private key used.
```sh
nile send <env_signer> <contract_identifier> <contract_method> [PARAM_1, PARAM2...]

Invoke transaction was sent.
Contract address: 0x07db6b52c8ab888183277bc6411c400136fe566c0eebfb96fffa559b2e60e794
Transaction hash: 0x1c
```

### `call` and `invoke`
Using `call` and `invoke`, we can perform read and write operations against our local node (or public one using the `--network mainnet` parameter). The syntax is:

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
click==8.0.1
python-dotenv==0.19.2
49 changes: 49 additions & 0 deletions src/nile/accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""nile common module."""
import os
import json

from nile.common import ACCOUNTS_FILENAME


def register(pubkey, address, index, network):
"""Register a new account."""
file = f"{network}.{ACCOUNTS_FILENAME}"

if exists(pubkey, network):
raise Exception(f"account-{index} already exists in {file}")

with open(file, "r") as fp:
accounts = json.load(fp)
accounts[pubkey] = {
"address":address,
"index":index
}
with open(file, 'w') as file:
json.dump(accounts, file)


def exists(pubkey, network):
"""Return whether an account exists or not."""
foo = next(load(pubkey, network), None)
return foo is not None


def load(pubkey, network):
"""Load account that matches a pubkey."""
file = f"{network}.{ACCOUNTS_FILENAME}"

if not os.path.exists(file):
with open(file, 'w') as fp:
json.dump({}, fp)

with open(file) as fp:
accounts = json.load(fp)
if pubkey in accounts:
yield accounts[pubkey]

def current_index(network):
file = f"{network}.{ACCOUNTS_FILENAME}"

with open(file) as fp:
accounts = json.load(fp)
return len(accounts.keys())
3 changes: 3 additions & 0 deletions src/nile/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ def init_command():
print("🗄 Creating project directory tree")

copy_tree(Path(__file__).parent.parent / "base_project", ".")

with open("accounts.json", "w") as file:
file.write("{}")

print("⛵️ Nile project ready! Try running:")
print("")
Expand Down
68 changes: 68 additions & 0 deletions src/nile/commands/proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Command to call or invoke StarkNet smart contracts."""
import os
import subprocess
import json
from dotenv import load_dotenv
load_dotenv()

from nile import deployments, accounts
from nile.common import GATEWAYS

from nile.signer import Signer

def proxy_setup_command(signer, network):
"""Deploy an Account contract for the given private key."""
signer = Signer(int(os.environ[signer]))
if accounts.exists(str(signer.public_key), network):
signer_data = next(accounts.load(str(signer.public_key), network))
signer.account = signer_data["address"]
signer.index = signer_data["index"]
else: # doesn't exist, have to deploy
signer.index = accounts.current_index(network)
subprocess.run(f"nile deploy Account {signer.public_key} --alias account-{signer.index}", shell=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not importing the module?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't even think about it when prototyping initially. Good catch, fixed it!

address, _ = next(deployments.load(f"account-{signer.index}", network))
# initialize account
subprocess.run(f"nile invoke account-{signer.index} initialize {address}", shell=True)
signer.account = address
accounts.register(signer.public_key, address, signer.index, network)

return signer


def send_command(signer, contract, method, params, network):
"""Sugared call to a contract passing by an Account contract."""
address, abi = next(deployments.load(contract, network))
return proxy_command(signer, [address, method] + list(params), network)

def proxy_command(signer, params, network):
"""Execute a tx going through an Account contract."""
# params are : to, selector_name, calldata
signer = proxy_setup_command(signer, network)

_, abi = next(deployments.load(f"account-{signer.index}", network))

command = [
"starknet",
"invoke",
"--address",
signer.account,
"--abi",
abi,
"--function",
"execute",
]

if network == "mainnet":
os.environ["STARKNET_NETWORK"] = "alpha"
else:
gateway_prefix = "feeder_gateway" if type == "call" else "gateway"
command.append(f"--{gateway_prefix}_url={GATEWAYS.get(network)}")

if len(params) > 0:
command.append("--inputs")
ingested_inputs = signer.get_inputs(params[0], params[1], params[2:])
command.extend([str(param) for param in ingested_inputs[0]])
command.append("--signature")
command.extend([str(sig_part) for sig_part in ingested_inputs[1]])

subprocess.check_call(command)
1 change: 1 addition & 0 deletions src/nile/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
TEMP_DIRECTORY = ".temp"
ABIS_DIRECTORY = f"{BUILD_DIRECTORY}/abis"
DEPLOYMENTS_FILENAME = "deployments.txt"
ACCOUNTS_FILENAME = "accounts.json"

GATEWAYS = {"localhost": "http://localhost:5000/"}

Expand Down
30 changes: 29 additions & 1 deletion src/nile/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from nile.commands.node import node_command
from nile.commands.test import test_command
from nile.commands.version import version_command
from nile.common import BUILD_DIRECTORY, DEPLOYMENTS_FILENAME
from nile.commands.proxy import proxy_command, proxy_setup_command, send_command
from nile.common import BUILD_DIRECTORY, DEPLOYMENTS_FILENAME, ACCOUNTS_FILENAME


@click.group()
Expand Down Expand Up @@ -44,6 +45,28 @@ def deploy(artifact, arguments, network, alias):
deploy_command(artifact, arguments, network, alias)


@cli.command()
@click.argument("signer", nargs=1)
@click.argument("contract_name", nargs=1)
@click.argument("method", nargs=1)
@click.argument("params", nargs=-1)
@click.option("--network", default="localhost")
def send(signer, contract_name, method, params, network):
send_command(signer, contract_name, method, params, network)

@cli.command()
@click.argument("signer", nargs=1)
@click.argument("params", nargs=-1)
@click.option("--network", default="localhost")
def proxy(signer, params, network):
proxy_command(signer, params, network)

@cli.command()
@click.argument("signer", nargs=1)
@click.option("--network", default="localhost")
def setup(signer, network):
proxy_setup_command(signer, network)

@cli.command()
@click.argument("contract_name", nargs=1)
@click.argument("method", nargs=1)
Expand Down Expand Up @@ -104,11 +127,16 @@ def compile(contracts):
def clean():
"""Remove default build directory."""
local_deployments_filename = f"localhost.{DEPLOYMENTS_FILENAME}"
local_accounts_filename = f"localhost.{ACCOUNTS_FILENAME}"

if os.path.exists(local_deployments_filename):
print(f"🚮 Deleting {local_deployments_filename}")
os.remove(local_deployments_filename)

if os.path.exists(local_accounts_filename):
print(f"🚮 Deleting {local_accounts_filename}")
os.remove(local_accounts_filename)

if os.path.exists(BUILD_DIRECTORY):
print(f"🚮 Deleting {BUILD_DIRECTORY} directory")
shutil.rmtree(BUILD_DIRECTORY)
Expand Down
63 changes: 63 additions & 0 deletions src/nile/signer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Utility for sending signed transactions to an Account on Starknet."""
import subprocess

from starkware.crypto.signature.signature import private_to_stark_key, sign
from starkware.starknet.public.abi import get_selector_from_name
from starkware.cairo.common.hash_state import compute_hash_on_elements

class Signer():
"""
Utility for sending signed transactions to an Account on Starknet.

Parameters
----------

private_key : int

Examples
---------
Constructing a Singer object

>>> signer = Signer(1234)

Sending a transaction

>>> await signer.send_transaction(account,
account.contract_address,
'set_public_key',
[other.public_key]
)

"""

def __init__(self, private_key):
self.private_key = private_key
self.public_key = private_to_stark_key(private_key)
self.account = None
self.index = 0

def sign(self, message_hash):
return sign(msg_hash=message_hash, priv_key=self.private_key)

def get_nonce(self):
nonce = subprocess.check_output(f"nile call account-{self.index} get_nonce", shell=True, encoding='utf-8')
return int(nonce)

def get_inputs(self, to, selector_name, calldata):
nonce = self.get_nonce()
selector = get_selector_from_name(selector_name)
ingested_calldata = [int(arg, 16) for arg in calldata]
message_hash = hash_message(int(self.account,16), int(to,16), selector, ingested_calldata, nonce)
sig_r, sig_s = self.sign(message_hash)
return ((int(to,16), selector, len(ingested_calldata), *ingested_calldata, nonce), (sig_r, sig_s))


def hash_message(sender, to, selector, calldata, nonce):
message = [
sender,
to,
selector,
compute_hash_on_elements(calldata),
nonce
]
return compute_hash_on_elements(message)