├── .gitignore ├── 1-print.py ├── 2-print-and-notify.py ├── 3-storage.py ├── 4-domain.py ├── LICENSE.md ├── README.md ├── Workshop-Agenda.md └── wip ├── 3-trigger.py └── utils ├── __init__.py └── txio.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.avm 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /1-print.py: -------------------------------------------------------------------------------- 1 | """ 2 | In prompt.py, you need to execute `config sc-events on` to see the events showing up. 3 | 4 | Test & Build: 5 | neo> sc build_run 1-print.py True False False 07 05 6 | """ 7 | 8 | def Main(): 9 | print("Hello World") 10 | -------------------------------------------------------------------------------- /2-print-and-notify.py: -------------------------------------------------------------------------------- 1 | """ 2 | In prompt.py, you need to execute `config sc-events on` to see the events showing up. 3 | 4 | Test & Build: 5 | neo> sc build_run 2-print-and-notify.py True False False 07 05 6 | """ 7 | from boa.interop.Neo.Runtime import Log, Notify 8 | 9 | def Main(): 10 | # Print translates to a `Log` call, and is best used with simple strings for 11 | # development info. To print variables such as lists and objects, use `Notify`. 12 | print("log via print (1)") 13 | Log("normal log (2)") 14 | Notify("notify (3)") 15 | 16 | # Sending multiple arguments as notify payload: 17 | msg = ["a", 1, 2, b"3"] 18 | Notify(msg) 19 | -------------------------------------------------------------------------------- /3-storage.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to write, read and manipulate value in storage. 3 | 4 | It is also a good example of using neo-python's `debugstorage`, which 5 | allows you to test `Put` operations with `build .. test` commands. 6 | Debugstorage is enabled by default, you can disable it with 7 | `sc debugstorage off` and, more importantly, reset it with 8 | `sc debugstorage reset`. 9 | 10 | Test & Build: 11 | neo> sc build_run 3-storage.py True False False 07 05 12 | 13 | Invoke this multiple times to see an increasing value in storage. Reset with: 14 | 15 | neo> sc debugstorage reset 16 | """ 17 | from boa.interop.Neo.Runtime import Log, Notify 18 | from boa.interop.Neo.Storage import Get, Put, GetContext 19 | 20 | def Main(): 21 | context = GetContext() 22 | 23 | # This is the storage key we use in this example 24 | item_key = 'test-storage-key' 25 | 26 | # Try to get a value for this key from storage 27 | item_value = Get(context, item_key) 28 | msg = ["Value read from storage:", item_value] 29 | Notify(msg) 30 | 31 | if len(item_value) == 0: 32 | Notify("Storage key not yet set. Setting to 1") 33 | item_value = 1 34 | 35 | else: 36 | Notify("Storage key already set. Incrementing by 1") 37 | item_value += 1 38 | 39 | # Store the new value 40 | Put(context, item_key, item_value) 41 | msg = ["New value written into storage:", item_value] 42 | Notify(msg) 43 | 44 | return item_value 45 | -------------------------------------------------------------------------------- /4-domain.py: -------------------------------------------------------------------------------- 1 | """ 2 | !NOTE! Current versions of neo-python/neo-boa does not calculate fees correctly, 3 | deploy with an extra network fee as contract is > 1024 bytes (--fee=0.1, see "Importing") 4 | 5 | Testing: 6 | 7 | neo> sc build_run 4-domain.py True False False 0710 05 query ["test.com"] 8 | neo> sc build_run 4-domain.py True False False 0710 05 register ["test.com","AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"] 9 | neo> sc build_run 4-domain.py True False False 0710 05 delete ["test.com"] 10 | neo> sc build_run 4-domain.py True False False 0710 05 transfer ["test.com","AK2nJJpJr6o664CWJKi1QRXjqeic"] 11 | 12 | 13 | Importing: 14 | 15 | neo> sc deploy 4-domain.avm True False False 0710 05 --fee=0.1 16 | neo> search contract ... 17 | 18 | Using: 19 | 20 | neo> testinvoke 5030694901a527908ab0a1494670109e7b85e3e4 query ["test.com"] 21 | neo> testinvoke 5030694901a527908ab0a1494670109e7b85e3e4 register ["test.com","AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"] 22 | neo> testinvoke 5030694901a527908ab0a1494670109e7b85e3e4 delete ["test.com"] 23 | neo> testinvoke 5030694901a527908ab0a1494670109e7b85e3e4 transfer ["test.com","AZ9Bmz6qmboZ4ry1z8p2KF3ftyA2ckJAym"] 24 | """ 25 | from boa.interop.Neo.Runtime import Log, Notify 26 | from boa.interop.Neo.Storage import Get, Put, GetContext 27 | from boa.interop.Neo.Runtime import GetTrigger,CheckWitness 28 | from boa.builtins import concat 29 | 30 | 31 | def Main(operation, args): 32 | nargs = len(args) 33 | if nargs == 0: 34 | print("No domain name supplied") 35 | return 0 36 | 37 | if operation == 'query': 38 | domain_name = args[0] 39 | return QueryDomain(domain_name) 40 | 41 | elif operation == 'delete': 42 | domain_name = args[0] 43 | return DeleteDomain(domain_name) 44 | 45 | elif operation == 'register': 46 | if nargs < 2: 47 | print("required arguments: [domain_name] [owner]") 48 | return 0 49 | domain_name = args[0] 50 | owner = args[1] 51 | return RegisterDomain(domain_name, owner) 52 | 53 | elif operation == 'transfer': 54 | if nargs < 2: 55 | print("required arguments: [domain_name] [to_address]") 56 | return 0 57 | domain_name = args[0] 58 | to_address = args[1] 59 | return TransferDomain(domain_name, to_address) 60 | 61 | 62 | def QueryDomain(domain_name): 63 | msg = concat("QueryDomain: ", domain_name) 64 | Notify(msg) 65 | 66 | context = GetContext() 67 | owner = Get(context, domain_name) 68 | if not owner: 69 | Notify("Domain is not yet registered") 70 | return False 71 | 72 | Notify(owner) 73 | return owner 74 | 75 | 76 | def RegisterDomain(domain_name, owner): 77 | msg = concat("RegisterDomain: ", domain_name) 78 | Notify(msg) 79 | 80 | if not CheckWitness(owner): 81 | Notify("Owner argument is not the same as the sender") 82 | return False 83 | 84 | context = GetContext() 85 | exists = Get(context, domain_name) 86 | if exists: 87 | Notify("Domain is already registered") 88 | return False 89 | 90 | Put(context, domain_name, owner) 91 | return True 92 | 93 | 94 | def TransferDomain(domain_name, to_address): 95 | msg = concat("TransferDomain: ", domain_name) 96 | Notify(msg) 97 | 98 | context = GetContext() 99 | owner = Get(context, domain_name) 100 | if not owner: 101 | Notify("Domain is not yet registered") 102 | return False 103 | 104 | if not CheckWitness(owner): 105 | Notify("Sender is not the owner, cannot transfer") 106 | return False 107 | 108 | if not len(to_address) != 34: 109 | Notify("Invalid new owner address. Must be exactly 34 characters") 110 | return False 111 | 112 | Put(context, domain_name, to_address) 113 | return True 114 | 115 | 116 | def DeleteDomain(domain_name): 117 | msg = concat("DeleteDomain: ", domain_name) 118 | Notify(msg) 119 | 120 | context = GetContext() 121 | owner = Get(context, domain_name) 122 | if not owner: 123 | Notify("Domain is not yet registered") 124 | return False 125 | 126 | if not CheckWitness(owner): 127 | Notify("Sender is not the owner, cannot transfer") 128 | return False 129 | 130 | Delete(context, domain_name) 131 | return True 132 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 City of Zion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Links 2 | 3 | * [Slides](https://goo.gl/3zve4E) 4 | * [Links, Infos and Resources](https://goo.gl/SRw1nd) <-- this document has a lot of information and links, be sure to take a look! 5 | * [Video: Introduction to Smart Contracts with Python 3.6 on the NEO Platform](https://youtu.be/ZZXz261AXrM) (by Tom Saunders) -- must watch video, introduction into neo-python and neo-boa 6 | * [neo-python documentation](http://neo-python.readthedocs.io/en/latest/index.html) 7 | * [Video of this workshop at DevCon 2018](https://www.youtube.com/watch?v=sk8tu1uqRDI) 8 | 9 | If you have any issues or ideas for improvements, please leave your feedback on the [GitHub repository](https://github.com/CityOfZion/python-smart-contract-workshop) and on the [NEO Discord](https://discord.gg/R8v48YA). 10 | 11 | 12 | ## Steps in the workshop 13 | 14 | 1. Setup [neo-python](https://github.com/CityOfZion/neo-python) and a [neo-privatenet](https://hub.docker.com/r/cityofzion/neo-privatenet/) Docker container [optionally with neoscan](https://hub.docker.com/r/cityofzion/neo-privatenet) 15 | 2. First smart contract, just printing "Hello World": [1-print.py](https://github.com/CityOfZion/python-smart-contract-workshop/blob/master/1-print.py) 16 | * Learn using neo-python's `build` command with the `test` argument 17 | * Test differences between Log and Notify 18 | 2. First smart contract using `print`, `Runtime.Log` and `Runtime.Notify`: [2-print-and-notify.py](https://github.com/CityOfZion/python-smart-contract-workshop/blob/master/2-print-and-notify.py) 19 | * Learn using neo-python's `build` command with the `test` argument 20 | * Test differences between Log and Notify 21 | 3. Basic smart contract using storage: [3-storage.py](https://github.com/CityOfZion/python-smart-contract-workshop/blob/master/3-storage.py) 22 | * Storage is one of the key components of most smart contracts 23 | * Everything is handled as bytes 24 | * Learn about `debugstorage on/off/reset` 25 | 4. Check out [Dictionary support](https://github.com/CityOfZion/neo-boa/blob/master/boa_test/example/DictTest2.py) and [`neo.Runtime.Serialize`](https://github.com/CityOfZion/neo-boa/blob/master/boa_test/example/demo/SerializationTest.py) 26 | 5. A domain registration smart contract: [4-domain.py](https://github.com/CityOfZion/python-smart-contract-workshop/blob/master/4-domain.py) 27 | * users can query, register, transfer and delete domains 28 | * important concept: checking of ownership 29 | 6. NEX ICO template: https://github.com/neonexchange/neo-ico-template 30 | 31 | **Note**: Inside neo-python's `prompt.py` you need to run `config sc-events on` to see any kind of notifications of the examples! 32 | 33 | ## Recommended System Setup 34 | 35 | Linux or Mac is recommended, and you need Python 3.6+. If you are using Windows, either setup a VM or use the Linux Subsystem (see also [here](https://medium.com/@gubanotorious/installing-and-running-neo-python-on-windows-10-284fb518b213) for more infos). 36 | 37 | Clone neo-python and setup everything as described in the README. Then create a symlink of this workshop folder to `neo-python/sc`, which makes it easier to import, build and execute the smart contracts in this workshop. 38 | 39 | Always work with a private network with this Docker image: https://hub.docker.com/r/cityofzion/neo-privatenet 40 | You can also easily run the private network with neoscan - just use 41 | [this Docker compose file](https://github.com/slipo/neo-scan-docker/blob/master/docker-compose.yml): 42 | 43 | $ wget https://raw.githubusercontent.com/slipo/neo-scan-docker/master/docker-compose.yml -O docker-compose-neoscan.yml 44 | $ docker-compose -f docker-compose-neoscan.yml up 45 | 46 | See [here](https://github.com/slipo/neo-scan-docker) for more information. 47 | 48 | 49 | ## Quickstart 50 | 51 | ```shell 52 | # Clone the workshop repository 53 | git clone https://github.com/CityOfZion/python-smart-contract-workshop.git 54 | cd python-smart-contract-workshop 55 | 56 | # Pull the Docker image 57 | docker pull cityofzion/neo-privatenet 58 | 59 | # Start a private network 60 | docker run --rm -d --name neo-privatenet -p 20333-20336:20333-20336/tcp -p 30333-30336:30333-30336/tcp cityofzion/neo-privatenet 61 | 62 | # Download the private network wallet 63 | wget https://s3.amazonaws.com/neo-experiments/neo-privnet.wallet 64 | 65 | # Create a Python 3.6 virtual environment and activate it 66 | python3.6 -m venv venv 67 | . venv/bin/activate 68 | 69 | # Install neo-python 70 | pip install neo-python 71 | 72 | # Remove any old private chain database 73 | rm -rf ~/.neopython/Chains/privnet* 74 | 75 | # Start neo-python connected to the private net (-p), showing sc events (-v) 76 | np-prompt -p -v 77 | ``` 78 | 79 | 80 | ## Typical method signatures 81 | ```python 82 | # These two are just examples for playing around and experimenting: 83 | def Main(): 84 | def Main(operation): 85 | 86 | # This is how most real smart contracts look like: 87 | def Main(operation, args): 88 | ``` 89 | 90 | See also: [parameter & return value types](https://github.com/neo-project/docs/blob/master/en-us/sc/Parameter.md) 91 | 92 | ## Often used imports 93 | ```python 94 | from boa.interop.Neo.Runtime import Log, Notify 95 | from boa.interop.Neo.Storage import Get, Put, GetContext 96 | from boa.interop.Neo.Runtime import GetTrigger,CheckWitness 97 | from boa.builtins import concat, list, range, take, substr 98 | ``` 99 | 100 | ## Often used `build` commands 101 | ```shell 102 | neo> build sc/1-print.py test 07 05 True False 103 | neo> build sc/2-print-and-notify.py test 07 05 True False 104 | neo> build sc/3-storage.py test 07 05 True False 105 | neo> build sc/4-domain.py test 0710 05 True False query ["test.com"] 106 | ``` 107 | 108 | 109 | ## Useful code snippets 110 | 111 | * [neo-boa examples](https://github.com/CityOfZion/neo-boa/tree/master/boa_test/example) 112 | * https://github.com/neonexchange/neo-ico-template 113 | * https://github.com/neo-project/neo/wiki/Network-Protocol 114 | 115 | 116 | #### Timestamps 117 | 118 | You can get the last block timestamp from the blockchain with this code. 119 | ```python 120 | def now(): 121 | height = GetHeight() 122 | current_block = GetHeader(height) 123 | return current_block.Timestamp 124 | ``` 125 | 126 | Might not work with neo-boa 0.2.2, downgrade to 0.2.1 (see also https://github.com/CityOfZion/neo-boa/issues/35). 127 | 128 | 129 | #### Random numbers 130 | 131 | See https://medium.com/proof-of-working/coz-first-dapps-competition-dapp-review-3a6b284afaef#414c 132 | -------------------------------------------------------------------------------- /Workshop-Agenda.md: -------------------------------------------------------------------------------- 1 | * **Overview** 2 | * what are smart contracts, dev restrictions 3 | * Development tools: neo-boa & neo-python 4 | 5 | * **Hands-On 1** 6 | * Check that Python 3.6+ is installed 7 | * Clone and setup neo-python and neo-boa 8 | * Pull the privnet Docker image 9 | * Run Docker privnet and connect neo-python to it 10 | * use help, open wallet & rebuild 11 | 12 | * **Smart contract internals 1** 13 | * Supported Python built-ins and functions by neo-boa 14 | * https://github.com/CityOfZion/neo-boa/blob/master/boa/code/vmtoken.py#L735 // boa.code.builtins 15 | * Runtime.Log + Notify 16 | * print —> Neo.Runtime.Log 17 | * Parameter & return value types: https://github.com/neo-project/docs/blob/master/en-us/sc/tutorial/Parameter.md 18 | * neo-python `build` command 19 | 20 | * **Hands-On 2** 21 | * Very simple Print example 22 | * neo-python build & test process 23 | * See also: Smart Contract Parameters and Return Values 24 | 25 | * **Smart contract internals 2** 26 | * Costs for deploying and running smart contracts: http://docs.neo.org/en-us/sc/systemfees.html 27 | * Storage 28 | * you can only store a bytearray, int, and strings in storage. 29 | * if you want to store more complex objects i'd take a look at the serialization example ([[1]](https://github.com/CityOfZion/neo-boa/blob/master/boa/tests/src/SerializationTest.py), [[2]](https://github.com/CityOfZion/neo-boa/blob/master/boa/tests/src/SerializationTest2.py)) 30 | * CheckWitness 31 | 32 | * **Hands On 3** 33 | * Domain registration system 34 | * Deploy to privnet, invoke methods 35 | 36 | * **Smart contract internals 3** 37 | * TriggerType.Verification and TriggerType.Application 38 | * timestamps + random numbers 39 | * timestamps/block time: boa/src/tests/blockchain // from boa.blockchain.vm.Neo.Header import GetTimestamp 40 | * random numbers: docs from ambethia on first dapp comp project ([reference](https://medium.com/proof-of-working/coz-first-dapps-competition-dapp-review-3a6b284afaef#414c)) 41 | * [NEP-5 token standard](https://github.com/neo-project/proposals/blob/master/nep-5.mediawiki) 42 | 43 | * **Hands-On 4** 44 | * [NEX ICO template](https://github.com/neonexchange/neo-ico-template) 45 | -------------------------------------------------------------------------------- /wip/3-trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example which allows calls from the owner, or from users if at 3 | least 1 GAS is attached. THIS IS WORK IN PROGRESS AND NOT WORKING. 4 | 5 | The Verification trigger is basically only called when using the 6 | mintToken method in neo-python, so the only really good example 7 | is a NEP-5 ICO smart contract such as the NEX template. 8 | 9 | To do an invoke with verification and token, first you'll have 10 | to import the token to the wallet and then use `tkn_mint`: 11 | 12 | import token {contract_hash} 13 | wallet tkn_mint {token_symbol} {ADDR} --attach-neo=X 14 | 15 | Verification only happens on the blockchain with deployed contracts. 16 | The `build` command only does the Appication portion. 17 | 18 | neo> build sc/3-trigger.py test 0710 05 True False main [] 19 | neo> import contract sc/3-trigger.avm 0710 05 True False 20 | neo> testinvoke e2f8eabbb31323569e5071e9fc88f4eeaddb8aa4 main [] 21 | """ 22 | from boa.blockchain.vm.Neo.Runtime import Log, Notify 23 | from boa.blockchain.vm.Neo.Runtime import GetTrigger, CheckWitness 24 | from boa.blockchain.vm.Neo.TriggerType import Application, Verification 25 | from utils.txio import get_asset_attachments 26 | 27 | OWNER = b'#\xba\'\x03\xc52c\xe8\xd6\xe5"\xdc2 39\xdc\xd8\xee\xe9' 28 | 29 | 30 | def Main(operation, args): 31 | trigger = GetTrigger() 32 | 33 | if trigger == Verification: 34 | print("doing verification!") 35 | Notify("doing verification notify!") 36 | 37 | # is_owner = CheckWitness(OWNER) 38 | # if is_owner: 39 | # return True 40 | 41 | # Check that at least 1 GAS is attached 42 | attachment = get_asset_attachments() 43 | if attachment.gas_attached >= 1: 44 | return True 45 | return False 46 | 47 | elif trigger == Application: 48 | print("doing application!") 49 | 50 | return 1 51 | -------------------------------------------------------------------------------- /wip/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/python-smart-contract-workshop/a25aca25653fe1219c838561ae97c3ab407956ff/wip/utils/__init__.py -------------------------------------------------------------------------------- /wip/utils/txio.py: -------------------------------------------------------------------------------- 1 | from boa.blockchain.vm.System.ExecutionEngine import GetScriptContainer, GetExecutingScriptHash 2 | from boa.blockchain.vm.Neo.Transaction import Transaction, GetReferences, GetOutputs,GetUnspentCoins 3 | from boa.blockchain.vm.Neo.Output import GetValue, GetAssetId, GetScriptHash 4 | 5 | class Attachments(): 6 | """ 7 | Container object ( struct ) for passing around information about attached neo and gas 8 | """ 9 | neo_attached = 0 10 | 11 | gas_attached = 0 12 | 13 | sender_addr = 0 14 | 15 | receiver_addr = 0 16 | 17 | neo_asset_id = b'\x9b|\xff\xda\xa6t\xbe\xae\x0f\x93\x0e\xbe`\x85\xaf\x90\x93\xe5\xfeV\xb3J\\"\x0c\xcd\xcfn\xfc3o\xc5' 18 | 19 | gas_asset_id = b'\xe7-(iy\xeel\xb1\xb7\xe6]\xfd\xdf\xb2\xe3\x84\x10\x0b\x8d\x14\x8ewX\xdeB\xe4\x16\x8bqy,`' 20 | 21 | 22 | 23 | def get_asset_attachments() -> Attachments: 24 | """ 25 | Gets information about NEO and Gas attached to an invocation TX 26 | 27 | :return: 28 | Attachments: An object with information about attached neo and gas 29 | """ 30 | attachment = Attachments() 31 | 32 | tx = GetScriptContainer() # type:Transaction 33 | references = tx.References 34 | attachment.receiver_addr = GetExecutingScriptHash() 35 | 36 | if len(references) > 0: 37 | 38 | reference = references[0] 39 | attachment.sender_addr = reference.ScriptHash 40 | 41 | sent_amount_neo = 0 42 | sent_amount_gas = 0 43 | 44 | for output in tx.Outputs: 45 | if output.ScriptHash == attachment.receiver_addr and output.AssetId == attachment.neo_asset_id: 46 | sent_amount_neo += output.Value 47 | 48 | if output.ScriptHash == attachment.receiver_addr and output.AssetId == attachment.gas_asset_id: 49 | sent_amount_gas += output.Value 50 | 51 | attachment.neo_attached = sent_amount_neo 52 | attachment.gas_attached = sent_amount_gas 53 | 54 | 55 | return attachment 56 | --------------------------------------------------------------------------------