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}")