Skip to content

Commit

Permalink
Merge branch 'dev' into example/simple_scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
nielstron authored Mar 8, 2023
2 parents 510794f + 4e85c59 commit 5466faa
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ venv
.vscode
__tmp_eopsin.py
test.py
test/
build/
*.uplc
*.cbor
*.plutus
Expand Down
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
- >
Expand Down
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 *

Expand Down Expand Up @@ -153,5 +182,6 @@ Donation in ADA can be submitted to `$imperatorlang` or `addr1qyz3vgd5xxevjy2rvq

### Supporters

<a href="https://github.com/inversion-dev"><img src="https://avatars.githubusercontent.com/u/127298233?s=200&v=4" width="50></a>
<a href="https://github.com/MuesliSwapTeam/"><img src="https://avatars.githubusercontent.com/u/91151317?v=4" width="50" /></a>
<a href="https://github.com/AadaFinance/"><img src="https://avatars.githubusercontent.com/u/89693711?v=4" width="50" /></a>
2 changes: 1 addition & 1 deletion eopsin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 14 additions & 8 deletions eopsin/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions eopsin/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
47 changes: 43 additions & 4 deletions eopsin/rewrite/rewrite_import.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import importlib
import importlib.util
import pathlib
import typing
import sys
from ast import *

from ..util import CompilingNodeTransformer
Expand All @@ -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]]:
Expand All @@ -28,14 +62,19 @@ def visit_ImportFrom(
node.names[0].asname == None
), "The import must have the form 'from <pkg> 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
2 changes: 1 addition & 1 deletion eopsin/type_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion eopsin/typed_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion eopsin/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

0 comments on commit 5466faa

Please sign in to comment.