Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting to save development artifacts and store them in a map #590

Merged
merged 20 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from 16 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
5 changes: 5 additions & 0 deletions brownie/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def __init__(self):
raise ValueError(f"Multiple networks using ID '{key}'")
self.networks[key] = value

# make sure chainids are always strings
for network, values in self.networks.items():
if "chainid" in values:
self.networks[network]["chainid"] = str(values["chainid"])

self.argv = defaultdict(lambda: None)
self.settings = _Singleton("settings", (ConfigDict,), {})(base_config)
self._active_network = None
Expand Down
1 change: 1 addition & 0 deletions brownie/data/default-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ hypothesis:

autofetch_sources: false
dependencies: null
dev_deployment_artifacts: false
29 changes: 22 additions & 7 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,23 +386,38 @@ def balance(self) -> Wei:
return Wei(balance)

def _deployment_path(self) -> Optional[Path]:
if CONFIG.network_type != "live" or not self._project._path:
if not self._project._path or (
CONFIG.network_type != "live" and not CONFIG.settings["dev_deployment_artifacts"]
):
return None
chainid = CONFIG.active_network["chainid"]

chainid = CONFIG.active_network["chainid"] if CONFIG.network_type == "live" else "dev"
path = self._project._build_path.joinpath(f"deployments/{chainid}")
path.mkdir(exist_ok=True)
return path.joinpath(f"{self.address}.json")

def _save_deployment(self) -> None:
path = self._deployment_path()
if path and not path.exists():
with path.open("w") as fp:
json.dump(self._build, fp)
chainid = CONFIG.active_network["chainid"] if CONFIG.network_type == "live" else "dev"
deployment_build = self._build.copy()

deployment_build["deployment"] = {
"address": self.address,
"chainid": chainid,
"blockHeight": web3.eth.blockNumber,
}
if path:
self._project._add_to_deployment_map(self)
if not path.exists():
with path.open("w") as fp:
json.dump(deployment_build, fp)

def _delete_deployment(self) -> None:
path = self._deployment_path()
if path and path.exists():
path.unlink()
if path:
self._project._remove_from_deployment_map(self)
if path.exists():
path.unlink()
iamdefinitelyahuman marked this conversation as resolved.
Show resolved Hide resolved


class Contract(_DeployedContractBase):
Expand Down
2 changes: 1 addition & 1 deletion brownie/network/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def connect(network: str = None, launch_rpc: bool = True) -> None:
rpc.launch(active["cmd"], **active["cmd_settings"])
else:
Accounts()._reset()
if CONFIG.network_type == "live":
if CONFIG.network_type == "live" or CONFIG.settings["dev_deployment_artifacts"]:
for p in project.get_loaded_projects():
p._load_deployments()

Expand Down
100 changes: 94 additions & 6 deletions brownie/project/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
InterfaceContainer,
ProjectContract,
)
from brownie.network.rpc import _revert_register
from brownie.network.state import _add_contract, _remove_contract
from brownie.project import compiler, ethpm
from brownie.project.build import BUILD_KEYS, INTERFACE_KEYS, Build
Expand Down Expand Up @@ -223,6 +224,10 @@ def load(self) -> None:
sys.modules["__main__"].__dict__,
sys.modules["brownie.project"].__dict__,
]

# register project for revert and reset
_revert_register(self)

self._active = True
_loaded_projects.append(self)

Expand Down Expand Up @@ -296,30 +301,107 @@ def _compile_interfaces(self, compiled_hashes: Dict) -> None:
self._build._add(abi)

def _load_deployments(self) -> None:
if CONFIG.network_type != "live":
if CONFIG.network_type != "live" and not CONFIG.settings["dev_deployment_artifacts"]:
return
chainid = CONFIG.active_network["chainid"]
chainid = CONFIG.active_network["chainid"] if CONFIG.network_type == "live" else "dev"
path = self._build_path.joinpath(f"deployments/{chainid}")
path.mkdir(exist_ok=True)
deployments = list(path.glob("*.json"))
deployments.sort(key=lambda k: k.stat().st_mtime)
deployment_map: Dict = {}
for build_json in deployments:
with build_json.open() as fp:
build = json.load(fp)
if build["contractName"] not in self._containers:

contract_name = build["contractName"]
if contract_name not in self._containers:
build_json.unlink()
continue
if "pcMap" in build:
contract = ProjectContract(self, build, build_json.stem)
else:
contract = Contract( # type: ignore
build["contractName"], build_json.stem, build["abi"]
contract_name, build_json.stem, build["abi"]
)
contract._project = self
container = self._containers[build["contractName"]]
container = self._containers[contract_name]
_add_contract(contract)
container._contracts.append(contract)

deployment_map.setdefault(chainid, {}).setdefault(contract_name, []).insert(
0, build_json.stem
)

self._save_deployment_map(deployment_map)

def _clear_dev_deployments(self, height: int = 0) -> None:
matnad marked this conversation as resolved.
Show resolved Hide resolved
path = self._build_path.joinpath(f"deployments/dev")
if path.exists():
deployment_map = self._load_deployment_map()
for deployment in list(path.glob("*.json")):
matnad marked this conversation as resolved.
Show resolved Hide resolved
if height == 0:
deployment.unlink()
else:
with deployment.open("r") as fp:
deployment_artifact = json.load(fp)
block_height = deployment_artifact["deployment"]["blockHeight"]
address = deployment_artifact["deployment"]["address"]
contract_name = deployment_artifact["contractName"]
matnad marked this conversation as resolved.
Show resolved Hide resolved
if block_height > height:
deployment.unlink()
try:
deployment_map["dev"][contract_name].remove(address)
except (KeyError, ValueError):
pass
if "dev" in deployment_map and (height == 0 or not deployment_map["dev"]):
del deployment_map["dev"]
path.rmdir()
matnad marked this conversation as resolved.
Show resolved Hide resolved

self._save_deployment_map(deployment_map)

def _load_deployment_map(self) -> Dict:
deployment_map: Dict = {}
map_path = self._build_path.joinpath("deployments/map.json")
if map_path.exists():
with map_path.open("r") as fp:
deployment_map = json.load(fp)
return deployment_map

def _save_deployment_map(self, deployment_map: Dict) -> None:
with self._build_path.joinpath("deployments/map.json").open("w") as fp:
json.dump(deployment_map, fp, sort_keys=True, indent=2, default=sorted)

def _remove_from_deployment_map(self, contract: ProjectContract) -> None:
if CONFIG.network_type != "live" and not CONFIG.settings["dev_deployment_artifacts"]:
return
chainid = CONFIG.active_network["chainid"] if CONFIG.network_type == "live" else "dev"
deployment_map = self._load_deployment_map()
try:
deployment_map[chainid][contract._name].remove(contract.address)
if not deployment_map[chainid][contract._name]:
del deployment_map[chainid][contract._name]
if not deployment_map[chainid]:
del deployment_map[chainid]
except (KeyError, ValueError):
pass

self._save_deployment_map(deployment_map)

def _add_to_deployment_map(self, contract: ProjectContract) -> None:
if CONFIG.network_type != "live" and not CONFIG.settings["dev_deployment_artifacts"]:
return

chainid = CONFIG.active_network["chainid"] if CONFIG.network_type == "live" else "dev"
deployment_map = self._load_deployment_map()
try:
deployment_map[chainid][contract._name].remove(contract.address)
except (ValueError, KeyError):
pass
deployment_map.setdefault(chainid, {}).setdefault(contract._name, []).insert(
0, contract.address
)
self._save_deployment_map(deployment_map)

def _update_and_register(self, dict_: Any) -> None:
dict_.update(self)
if "interface" not in dict_:
Expand Down Expand Up @@ -391,6 +473,12 @@ def close(self, raises: bool = True) -> None:
except ValueError:
pass

def _revert(self, height: int) -> None:
self._clear_dev_deployments(height)

def _reset(self) -> None:
self._clear_dev_deployments()


class TempProject(_ProjectBase):

Expand Down Expand Up @@ -427,7 +515,7 @@ def check_for_project(path: Union[Path, str] = ".") -> Optional[Path]:
return None


def get_loaded_projects() -> List:
def get_loaded_projects() -> List["Project"]:
"""Returns a list of currently loaded Project objects."""
return _loaded_projects.copy()

Expand Down
68 changes: 68 additions & 0 deletions tests/project/main/test_deployment_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import json


def get_map(project) -> dict:
with project._build_path.joinpath("deployments/map.json").open("r") as fp:
content = json.load(fp)
return content


def get_dev_artifacts(project) -> list:
return list(project._build_path.joinpath("deployments/dev/").glob("*.json"))


def test_dev_deployment_map_content(testproject, BrownieTester, config, accounts):
config.settings["dev_deployment_artifacts"] = True

# deploy and verify deployment of first contract
BrownieTester.deploy(True, {"from": accounts[0]})
content = get_map(testproject)
assert isinstance(content, dict)

assert len(content["dev"]["BrownieTester"]) == 1
assert len(get_dev_artifacts(testproject)) == 1

# deploy and verify deployment of second contract
BrownieTester.deploy(True, {"from": accounts[0]})
address = BrownieTester[-1].address

content = get_map(testproject)
assert len(content["dev"]["BrownieTester"]) == 2
assert content["dev"]["BrownieTester"][0] == address

assert len(get_dev_artifacts(testproject)) == 2


def test_dev_deployment_map_clear_on_disconnect(
devnetwork, testproject, BrownieTester, config, accounts
):
config.settings["dev_deployment_artifacts"] = True

BrownieTester.deploy(True, {"from": accounts[0]})
devnetwork.disconnect()
content = get_map(testproject)
assert not content


def test_dev_deployment_map_clear_on_remove(testproject, BrownieTester, config, accounts):
config.settings["dev_deployment_artifacts"] = True

BrownieTester.deploy(True, {"from": accounts[0]})
BrownieTester.remove(BrownieTester[-1])

assert len(get_dev_artifacts(testproject)) == 0
content = get_map(testproject)
assert not content


def test_dev_deployment_map_revert(testproject, BrownieTester, config, accounts, rpc):
config.settings["dev_deployment_artifacts"] = True

BrownieTester.deploy(True, {"from": accounts[0]})
rpc.snapshot()
BrownieTester.deploy(True, {"from": accounts[0]})
assert len(get_dev_artifacts(testproject)) == 2
rpc.revert()
assert len(get_dev_artifacts(testproject)) == 1
content = get_map(testproject)
assert len(content["dev"]["BrownieTester"]) == 1