diff --git a/.gitignore b/.gitignore index 226dad1a..052121c3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ venv .vscode __tmp_eopsin.py test.py -test/ +build/ *.uplc *.cbor *.plutus diff --git a/.travis.yml b/.travis.yml index c7714f30..fa3bb33c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,11 @@ script: - coverage run --source=eopsin setup.py test # the samples from the README file - > - coverage run -a --source=eopsin -m eopsin eval examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 0, \"fields\": []}" + coverage run -a --source=eopsin -m eopsin eval examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 6, \"fields\": []}" - > coverage run -a --source=eopsin -m eopsin compile examples/smart_contracts/assert_sum.py > assert_sum.uplc - > - coverage run -a --source=eopsin -m eopsin eval_uplc examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 0, \"fields\": []}" + coverage run -a --source=eopsin -m eopsin eval_uplc examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 6, \"fields\": []}" - > coverage run -a --source=eopsin -m eopsin compile_pluto examples/smart_contracts/assert_sum.py - > diff --git a/README.md b/README.md index 4d39a24b..bd5752b7 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,16 @@ If the program compiles then: ### Getting Started +#### Example repository + +Check out this example repository for a quick start in setting up a development environment +and compiling some sample contracts yours: + +https://github.com/ImperatorLang/eopsin-example + +You can replace the contracts in your local copy of the repository with code from the +`examples` section here to start exploring different contracts. + #### Developer Community and Questions This repository contains a discussions page. @@ -38,6 +48,9 @@ Others may be able to help you and will also benefit from the previously shared Check out the community [here](https://github.com/ImperatorLang/eopsin/discussions) +Alternatively, feel free to join the welcoming discord community +at the TxPipe server: https://discord.gg/2ETSZnQQH9 + #### Installation Install Python 3.8. Then run @@ -50,16 +63,32 @@ python3.8 -m pip install eopsin-lang A short non-complete introduction in starting to write smart contracts follows. -1. Make sure you understand python. Eopsin works like python and uses python. There are tons of tutorials for python, choose what suits you best. -2. Make sure your contract is valid python and the types check out. Write simple contracts first and run them using `eopsin eval` to get a feeling for how they work. -3. Make sure your contract is valid eopsin code. Run `eopsin compile` and look at the compiler erros for guidance along what works and doesn't work and why. -4. Dig into the [`examples`](https://github.com/ImperatorLang/eopsin/tree/master/examples) to understand common patterns. Check out the [`prelude`](https://imperatorlang.github.io/eopsin/eopsin/prelude.html) for understanding how the Script Context is structured and how complex datums are defined. +1. Make sure you understand EUTxOs, Addresses, Validators etc on Cardano. [There is a wonderful crashcourse by @KtorZ](https://aiken-lang.org/fundamentals/eutxo). The contract will work on these concepts +2. Make sure you understand python. Eopsin works like python and uses python. There are tons of tutorials for python, choose what suits you best. +3. Make sure your contract is valid python and the types check out. Write simple contracts first and run them using `eopsin eval` to get a feeling for how they work. +4. Make sure your contract is valid eopsin code. Run `eopsin compile` and look at the compiler erros for guidance along what works and doesn't work and why. +5. Dig into the [`examples`](https://github.com/ImperatorLang/eopsin/tree/master/examples) to understand common patterns. Check out the [`prelude`](https://imperatorlang.github.io/eopsin/eopsin/prelude.html) for understanding how the Script Context is structured and how complex datums are defined. +6. Check out the [sample repository](https://github.com/ImperatorLang/eopsin-example) to find a sample setup for developing your own contract. + + +In summary, a smart contract in eopsin is defined by the function `validator` in your contract file. +The function validates that a specific value can be spent, minted, burned, withdrawn etc, depending +on where it is invoked/used as a credential. +If the function fails (i.e. raises an error of any kind such as a `KeyError` or `AssertionError`) +the validation is denied, and the funds can not be spent, minted, burned etc. + +> There is a subtle difference here in comparison to most other Smart Contract languages. +> In eopsin a validator may return anything (in particular also `False`) - as long as it does not fail, the execution is considered valid. +> This is more similar to how contracts in Solidity always pass, unless they run out of gas or hit an error. +> So make sure to `assert` what you want to ensure to hold for validation! A simple contract called the "Gift Contract" verifies that only specific wallets can withdraw money. They are authenticated by a signature. -See the [tutorial by `pycardano`](https://pycardano.readthedocs.io/en/latest/guides/plutus.html) for explanations on what each of the parameters to the validator means +If you don't understand what a pubkeyhash is and how this validates anything, check out [this gentle introduction into Cardanos EUTxO](https://aiken-lang.org/fundamentals/eutxo). +Also see the [tutorial by `pycardano`](https://pycardano.readthedocs.io/en/latest/guides/plutus.html) for explanations on what each of the parameters to the validator means and how to build transactions with the contract. + ```python3 from eopsin.prelude import * @@ -153,5 +182,6 @@ Donation in ADA can be submitted to `$imperatorlang` or `addr1qyz3vgd5xxevjy2rvq ### Supporters + diff --git a/eopsin/__init__.py b/eopsin/__init__.py index 8b7c27f5..b54cbaa1 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 3) +VERSION = (0, 9, 9) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" diff --git a/eopsin/__main__.py b/eopsin/__main__.py index 48c2632f..e53d14a3 100644 --- a/eopsin/__main__.py +++ b/eopsin/__main__.py @@ -93,9 +93,13 @@ def main(): source_code = f.read() if command == Command.eval: - with open("__tmp_eopsin.py", "w") as fp: - fp.write(source_code) - sc = importlib.import_module("__tmp_eopsin") + if args.input_file == "-": + with open("__tmp_eopsin.py", "w") as fp: + fp.write(source_code) + input_file = "__tmp_eopsin.py" + sys.path.append(str(pathlib.Path(input_file).parent.absolute())) + sc = importlib.import_module(pathlib.Path(input_file).stem) + sys.path.pop() print("Starting execution") print("------------------") try: @@ -110,14 +114,16 @@ def main(): print("------------------") print(ret) - source_ast = compiler.parse(source_code) + source_ast = compiler.parse(source_code, filename=input_file) if command == Command.parse: print("Parsed successfully.") return try: - code = compiler.compile(source_ast, force_three_params=args.force_three_params) + code = compiler.compile( + source_ast, filename=input_file, force_three_params=args.force_three_params + ) except CompilerError as c: # Generate nice error message from compiler error if not isinstance(c.node, ast.Module): @@ -168,10 +174,10 @@ def main(): "Please supply an output directory if no input file is specified." ) exit(-1) - target_dir = pathlib.Path(pathlib.Path(input_file).stem) + target_dir = pathlib.Path("build") / pathlib.Path(input_file).stem else: target_dir = pathlib.Path(args.output_directory) - target_dir.mkdir(exist_ok=True) + target_dir.mkdir(exist_ok=True, parents=True) uplc_dump = code.dumps() cbor_hex = pyaiken.uplc.flat(uplc_dump) # create cbor file for use with pycardano/lucid @@ -212,7 +218,7 @@ def main(): print("------------------") assert isinstance(code, uplc.ast.Program) try: - ret = uplc.dumps(uplc.eval(f)) + ret = uplc.dumps(uplc.eval(code)) except Exception as e: print("An exception was raised") ret = e diff --git a/eopsin/compiler.py b/eopsin/compiler.py index af651962..3df4bd93 100644 --- a/eopsin/compiler.py +++ b/eopsin/compiler.py @@ -839,10 +839,10 @@ def generic_visit(self, node: AST) -> plt.AST: raise NotImplementedError(f"Can not compile {node}") -def compile(prog: AST, force_three_params=False): +def compile(prog: AST, filename=None, force_three_params=False): rewrite_steps = [ # Important to call this one first - it imports all further files - RewriteImport(), + RewriteImport(filename=filename), # Rewrites that simplify the python code RewriteAugAssign(), RewriteTupleAssign(), diff --git a/eopsin/rewrite/rewrite_import.py b/eopsin/rewrite/rewrite_import.py index 1c991208..1624fd9b 100644 --- a/eopsin/rewrite/rewrite_import.py +++ b/eopsin/rewrite/rewrite_import.py @@ -1,6 +1,8 @@ import importlib +import importlib.util import pathlib import typing +import sys from ast import * from ..util import CompilingNodeTransformer @@ -10,9 +12,41 @@ """ +def import_module(name, package=None): + """An approximate implementation of import.""" + absolute_name = importlib.util.resolve_name(name, package) + try: + return sys.modules[absolute_name] + except KeyError: + pass + + path = None + if "." in absolute_name: + parent_name, _, child_name = absolute_name.rpartition(".") + parent_module = import_module(parent_name) + path = parent_module.__spec__.submodule_search_locations + for finder in sys.meta_path: + spec = finder.find_spec(absolute_name, path) + if spec is not None: + break + else: + msg = f"No module named {absolute_name!r}" + raise ModuleNotFoundError(msg, name=absolute_name) + module = importlib.util.module_from_spec(spec) + sys.modules[absolute_name] = module + spec.loader.exec_module(module) + if path is not None: + setattr(parent_module, child_name, module) + return module + + class RewriteImport(CompilingNodeTransformer): step = "Resolving imports" + def __init__(self, filename=None, package=None): + self.filename = filename + self.package = package + def visit_ImportFrom( self, node: ImportFrom ) -> typing.Union[ImportFrom, typing.List[AST]]: @@ -28,14 +62,19 @@ def visit_ImportFrom( node.names[0].asname == None ), "The import must have the form 'from import *'" # TODO set anchor point according to own package - module_file = pathlib.Path(importlib.import_module(node.module).__file__) + if self.filename: + sys.path.append(str(pathlib.Path(self.filename).parent.absolute())) + module = import_module(node.module, self.package) + if self.filename: + sys.path.pop() + module_file = pathlib.Path(module.__file__) assert ( module_file.suffix == ".py" ), "The import must import a single python file." # visit the imported file again - make sure that recursive imports are resolved accordingly with module_file.open("r") as fp: module_content = fp.read() - recursively_resolved: Module = self.visit( - parse(module_content, filename=module_file.name) - ) + recursively_resolved: Module = RewriteImport( + filename=str(module_file), package=module.__package__ + ).visit(parse(module_content, filename=module_file.name)) return recursively_resolved.body diff --git a/eopsin/type_inference.py b/eopsin/type_inference.py index db5cb861..0198c86a 100644 --- a/eopsin/type_inference.py +++ b/eopsin/type_inference.py @@ -242,7 +242,7 @@ def visit_If(self, node: If) -> TypedIf: ), "Can only cast instances, not classes" assert isinstance( target_inst_class.typ, UnionType - ), "Can only cast instances of Union types" + ), "Can only cast instances of Union types of PlutusData" assert isinstance(target_class, RecordType), "Can only cast to PlutusData" assert ( target_class in target_inst_class.typ.typs diff --git a/eopsin/typed_ast.py b/eopsin/typed_ast.py index 555d8614..c907cf2a 100644 --- a/eopsin/typed_ast.py +++ b/eopsin/typed_ast.py @@ -1023,7 +1023,15 @@ def empty_list(p: Type): el = empty_list(p.typ.typ) return plt.EmptyListList(uplc.BuiltinList([], el.sample_value)) if isinstance(p.typ, DictType): - plt.EmptyDataPairList() + return plt.EmptyListList( + uplc.BuiltinList( + [], + uplc.BuiltinPair( + uplc.PlutusConstr(0, FrozenList([])), + uplc.PlutusConstr(0, FrozenList([])), + ), + ) + ) if isinstance(p.typ, RecordType) or isinstance(p.typ, AnyType): return plt.EmptyDataList() raise NotImplementedError(f"Empty lists of type {p} can't be constructed yet") diff --git a/eopsin/util.py b/eopsin/util.py index 9e7a7843..84e94f89 100644 --- a/eopsin/util.py +++ b/eopsin/util.py @@ -552,5 +552,7 @@ def data_from_json(j: typing.Dict[str, typing.Any]) -> uplc.PlutusData: if "map" in j: return uplc.PlutusMap({d["k"]: d["v"] for d in j["map"]}) if "constructor" in j and "fields" in j: - return uplc.PlutusConstr(j["constructor"], j["fields"]) + return uplc.PlutusConstr( + j["constructor"], list(map(data_from_json, j["fields"])) + ) raise NotImplementedError(f"Unknown datum representation {j}")