.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Neo ICO Template
8 | A template for NEP5 Compliant Tokens on the NEO platform
9 |
10 |
11 | #### Considerations
12 |
13 | An article describing this template is available here:
14 |
15 | https://medium.com/neon-exchange/nex-ico-template-4ca7ba19fc8b
16 |
17 | #### Requirements
18 |
19 | Usage requires Python 3.6+
20 |
21 |
22 | #### Installation
23 |
24 | Clone the repository and navigate into the project directory.
25 | Make a Python 3 virtual environment and activate it via
26 |
27 | ```shell
28 | python3 -m venv venv
29 | source venv/bin/activate
30 | ```
31 |
32 | or to explicitly install Python 3.6 via
33 |
34 | virtualenv -p /usr/local/bin/python3.6 venv
35 | source venv/bin/activate
36 |
37 | Then install the requirements via
38 |
39 | ```shell
40 | pip install -r requirements.txt
41 | ```
42 |
43 | #### Compilation
44 |
45 | The template may be compiled as follows
46 |
47 | ```python
48 | from boa.compiler import Compiler
49 |
50 | Compiler.load_and_save('ico_template.py')
51 | ```
52 |
53 |
54 | This will compile your template to `ico_template.avm`
55 |
56 |
57 |
58 | #### Running tests
59 |
60 | 1. Install `requirements_test.txt`
61 |
62 | ```
63 | pip install -r requirements_test.txt
64 |
65 | ```
66 |
67 | 2. Run tests
68 |
69 | ```
70 | python -m unittest discover tests
71 | ```
72 |
73 | #### Testnet Deployed Details
74 |
75 | For testing purposes, this template is deployed on testnet with the following contract script hash:
76 |
77 | `0b6c1f919e95fe61c17a7612aebfaf4fda3a2214`
78 |
79 | ```json
80 | {
81 | "code": {
82 | "parameters": "0710",
83 | "hash": "0b6c1f919e95fe61c17a7612aebfaf4fda3a2214",
84 | "returntype": 5,
85 | "script": ".. omitted .."
86 | },
87 | "version": 0,
88 | "code_version": ".2",
89 | "name": "NEX Ico Template",
90 | "author": "localhuman",
91 | "description": "An ICO Template",
92 | "properties": {
93 | "dynamic_invoke": false,
94 | "storage": true
95 | },
96 | "email": "tom@neonexchange.org"
97 | }
98 | ```
99 |
100 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/__init__.py
--------------------------------------------------------------------------------
/compile.py:
--------------------------------------------------------------------------------
1 | from boa.compiler import Compiler
2 |
3 | Compiler.load_and_save('ico_template.py')
4 |
--------------------------------------------------------------------------------
/fixtures/coztest.db3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/fixtures/coztest.db3
--------------------------------------------------------------------------------
/fixtures/empty_fixture.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/fixtures/empty_fixture.tar.gz
--------------------------------------------------------------------------------
/fixtures/testwallet.db3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/fixtures/testwallet.db3
--------------------------------------------------------------------------------
/fixtures/testwallet2.db3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/fixtures/testwallet2.db3
--------------------------------------------------------------------------------
/fixtures/testwallet3.db3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/fixtures/testwallet3.db3
--------------------------------------------------------------------------------
/ico_template.py:
--------------------------------------------------------------------------------
1 | """
2 | NEX ICO Template
3 | ===================================
4 |
5 | Author: Thomas Saunders
6 | Email: tom@neonexchange.org
7 |
8 | Date: Dec 11 2017
9 |
10 | """
11 | from nex.txio import get_asset_attachments
12 | from nex.token import *
13 | from nex.crowdsale import *
14 | from nex.nep5 import *
15 | from boa.interop.Neo.Runtime import GetTrigger, CheckWitness
16 | from boa.interop.Neo.TriggerType import Application, Verification
17 | from boa.interop.Neo.Storage import *
18 |
19 | ctx = GetContext()
20 | NEP5_METHODS = ['name', 'symbol', 'decimals', 'totalSupply', 'balanceOf', 'transfer', 'transferFrom', 'approve', 'allowance']
21 |
22 |
23 | def Main(operation, args):
24 | """
25 |
26 | :param operation: str The name of the operation to perform
27 | :param args: list A list of arguments along with the operation
28 | :return:
29 | bytearray: The result of the operation
30 | """
31 |
32 | trigger = GetTrigger()
33 |
34 | # This is used in the Verification portion of the contract
35 | # To determine whether a transfer of system assets ( NEO/Gas) involving
36 | # This contract's address can proceed
37 | if trigger == Verification():
38 |
39 | # check if the invoker is the owner of this contract
40 | is_owner = CheckWitness(TOKEN_OWNER)
41 |
42 | # If owner, proceed
43 | if is_owner:
44 |
45 | return True
46 |
47 | # Otherwise, we need to lookup the assets and determine
48 | # If attachments of assets is ok
49 | attachments = get_asset_attachments()
50 | return can_exchange(ctx, attachments, True)
51 |
52 | elif trigger == Application():
53 |
54 | for op in NEP5_METHODS:
55 | if operation == op:
56 | return handle_nep51(ctx, operation, args)
57 |
58 | if operation == 'deploy':
59 | return deploy()
60 |
61 | elif operation == 'circulation':
62 | return get_circulation(ctx)
63 |
64 | # the following are handled by crowdsale
65 |
66 | elif operation == 'mintTokens':
67 | return perform_exchange(ctx)
68 |
69 | elif operation == 'crowdsale_register':
70 | return kyc_register(ctx, args)
71 |
72 | elif operation == 'crowdsale_status':
73 | return kyc_status(ctx, args)
74 |
75 | elif operation == 'crowdsale_available':
76 | return crowdsale_available_amount(ctx)
77 |
78 | elif operation == 'get_attachments':
79 | return get_asset_attachments()
80 |
81 | return 'unknown operation'
82 |
83 | return False
84 |
85 |
86 | def deploy():
87 | """
88 |
89 | :param token: Token The token to deploy
90 | :return:
91 | bool: Whether the operation was successful
92 | """
93 | if not CheckWitness(TOKEN_OWNER):
94 | print("Must be owner to deploy")
95 | return False
96 |
97 | if not Get(ctx, 'initialized'):
98 | # do deploy logic
99 | Put(ctx, 'initialized', 1)
100 | Put(ctx, TOKEN_OWNER, TOKEN_INITIAL_AMOUNT)
101 | return add_to_circulation(ctx, TOKEN_INITIAL_AMOUNT)
102 |
103 | return False
104 |
--------------------------------------------------------------------------------
/nex/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nash-io/neo-ico-template/0ad1af882ea470c77570cce0d27c43c4e49ec3b9/nex/__init__.py
--------------------------------------------------------------------------------
/nex/crowdsale.py:
--------------------------------------------------------------------------------
1 | from boa.interop.Neo.Blockchain import GetHeight
2 | from boa.interop.Neo.Runtime import CheckWitness
3 | from boa.interop.Neo.Action import RegisterAction
4 | from boa.interop.Neo.Storage import Get, Put
5 | from boa.builtins import concat
6 | from nex.token import *
7 | from nex.txio import get_asset_attachments
8 |
9 | # OnInvalidKYCAddress = RegisterAction('invalid_registration', 'address')
10 | OnKYCRegister = RegisterAction('kyc_registration', 'address')
11 | OnTransfer = RegisterAction('transfer', 'addr_from', 'addr_to', 'amount')
12 | OnRefund = RegisterAction('refund', 'addr_to', 'amount')
13 |
14 |
15 | def kyc_register(ctx, args):
16 | """
17 |
18 | :param args:list a list of addresses to register
19 | :param token:Token A token object with your ICO settings
20 | :return:
21 | int: The number of addresses to register for KYC
22 | """
23 | ok_count = 0
24 |
25 | if CheckWitness(TOKEN_OWNER):
26 |
27 | for address in args:
28 |
29 | if len(address) == 20:
30 |
31 | kyc_storage_key = concat(KYC_KEY, address)
32 | Put(ctx, kyc_storage_key, True)
33 |
34 | OnKYCRegister(address)
35 | ok_count += 1
36 |
37 | return ok_count
38 |
39 |
40 | def kyc_status(ctx, args):
41 | """
42 | Gets the KYC Status of an address
43 |
44 | :param args:list a list of arguments
45 | :return:
46 | bool: Returns the kyc status of an address
47 | """
48 |
49 | if len(args) > 0:
50 | addr = args[0]
51 |
52 | kyc_storage_key = concat(KYC_KEY, addr)
53 |
54 | return Get(ctx, kyc_storage_key)
55 |
56 | return False
57 |
58 |
59 | def perform_exchange(ctx):
60 | """
61 |
62 | :param token:Token The token object with NEP5/sale settings
63 | :return:
64 | bool: Whether the exchange was successful
65 | """
66 |
67 | attachments = get_asset_attachments() # [receiver, sender, neo, gas]
68 |
69 | # this looks up whether the exchange can proceed
70 | exchange_ok = can_exchange(ctx, attachments, False)
71 |
72 | if not exchange_ok:
73 | # This should only happen in the case that there are a lot of TX on the final
74 | # block before the total amount is reached. An amount of TX will get through
75 | # the verification phase because the total amount cannot be updated during that phase
76 | # because of this, there should be a process in place to manually refund tokens
77 | if attachments[2] > 0:
78 | OnRefund(attachments[1], attachments[2])
79 | # if you want to exchange gas instead of neo, use this
80 | # if attachments.gas_attached > 0:
81 | # OnRefund(attachments.sender_addr, attachments.gas_attached)
82 | return False
83 |
84 | # lookup the current balance of the address
85 | current_balance = Get(ctx, attachments[1])
86 |
87 | # calculate the amount of tokens the attached neo will earn
88 | exchanged_tokens = attachments[2] * TOKENS_PER_NEO / 100000000
89 |
90 | # if you want to exchange gas instead of neo, use this
91 | # exchanged_tokens += attachments[3] * TOKENS_PER_GAS / 100000000
92 |
93 | # add it to the the exchanged tokens and persist in storage
94 | new_total = exchanged_tokens + current_balance
95 | Put(ctx, attachments[1], new_total)
96 |
97 | # update the in circulation amount
98 | result = add_to_circulation(ctx, exchanged_tokens)
99 |
100 | # dispatch transfer event
101 | OnTransfer(attachments[0], attachments[1], exchanged_tokens)
102 |
103 | return True
104 |
105 |
106 | def can_exchange(ctx, attachments, verify_only):
107 | """
108 | Determines if the contract invocation meets all requirements for the ICO exchange
109 | of neo or gas into NEP5 Tokens.
110 | Note: This method can be called via both the Verification portion of an SC or the Application portion
111 |
112 | When called in the Verification portion of an SC, it can be used to reject TX that do not qualify
113 | for exchange, thereby reducing the need for manual NEO or GAS refunds considerably
114 |
115 | :param attachments:Attachments An attachments object with information about attached NEO/Gas assets
116 | :return:
117 | bool: Whether an invocation meets requirements for exchange
118 | """
119 |
120 | # if you are accepting gas, use this
121 | # if attachments[3] == 0:
122 | # print("no gas attached")
123 | # return False
124 |
125 | # if youre accepting neo, use this
126 |
127 | if attachments[2] == 0:
128 | return False
129 |
130 | # the following looks up whether an address has been
131 | # registered with the contract for KYC regulations
132 | # this is not required for operation of the contract
133 |
134 | # status = get_kyc_status(attachments.sender_addr, storage)
135 | if not get_kyc_status(ctx, attachments[1]):
136 | return False
137 |
138 | # caluclate the amount requested
139 | amount_requested = attachments[2] * TOKENS_PER_NEO / 100000000
140 |
141 | # this would work for accepting gas
142 | # amount_requested = attachments.gas_attached * token.tokens_per_gas / 100000000
143 |
144 | exchange_ok = calculate_can_exchange(ctx, amount_requested, attachments[1], verify_only)
145 |
146 | return exchange_ok
147 |
148 |
149 | def get_kyc_status(ctx, address):
150 | """
151 | Looks up the KYC status of an address
152 |
153 | :param address:bytearray The address to lookup
154 | :param storage:StorageAPI A StorageAPI object for storage interaction
155 | :return:
156 | bool: KYC Status of address
157 | """
158 | kyc_storage_key = concat(KYC_KEY, address)
159 |
160 | return Get(ctx, kyc_storage_key)
161 |
162 |
163 | def calculate_can_exchange(ctx, amount, address, verify_only):
164 | """
165 | Perform custom token exchange calculations here.
166 |
167 | :param amount:int Number of tokens to convert from asset to tokens
168 | :param address:bytearray The address to mint the tokens to
169 | :return:
170 | bool: Whether or not an address can exchange a specified amount
171 | """
172 | height = GetHeight()
173 |
174 | current_in_circulation = Get(ctx, TOKEN_CIRC_KEY)
175 |
176 | new_amount = current_in_circulation + amount
177 |
178 | if new_amount > TOKEN_TOTAL_SUPPLY:
179 | return False
180 |
181 | if height < BLOCK_SALE_START:
182 | return False
183 |
184 | # if we are in free round, any amount
185 | if height > LIMITED_ROUND_END:
186 | return True
187 |
188 | # check amount in limited round
189 | if amount <= MAX_EXCHANGE_LIMITED_ROUND:
190 |
191 | # check if they have already exchanged in the limited round
192 | r1key = concat(address, LIMITED_ROUND_KEY)
193 | has_exchanged = Get(ctx, r1key)
194 |
195 | # if not, then save the exchange for limited round
196 | if not has_exchanged:
197 | # note that this method can be invoked during the Verification trigger, so we have the
198 | # verify_only param to avoid the Storage.Put during the read-only Verification trigger.
199 | # this works around a "method Neo.Storage.Put not found in ->" error in InteropService.py
200 | # since Verification is read-only and thus uses a StateReader, not a StateMachine
201 | if not verify_only:
202 | Put(ctx, r1key, True)
203 | return True
204 |
205 | return False
206 |
207 | return False
208 |
--------------------------------------------------------------------------------
/nex/nep5.py:
--------------------------------------------------------------------------------
1 | from boa.interop.Neo.Runtime import CheckWitness, Notify
2 | from boa.interop.Neo.Action import RegisterAction
3 | from boa.interop.Neo.Storage import *
4 | from boa.builtins import concat
5 |
6 | from nex.token import *
7 |
8 |
9 | OnTransfer = RegisterAction('transfer', 'addr_from', 'addr_to', 'amount')
10 | OnApprove = RegisterAction('approve', 'addr_from', 'addr_to', 'amount')
11 |
12 |
13 | def handle_nep51(ctx, operation, args):
14 |
15 | if operation == 'name':
16 | return TOKEN_NAME
17 |
18 | elif operation == 'decimals':
19 | return TOKEN_DECIMALS
20 |
21 | elif operation == 'symbol':
22 | return TOKEN_SYMBOL
23 |
24 | elif operation == 'totalSupply':
25 | return Get(ctx, TOKEN_CIRC_KEY)
26 |
27 | elif operation == 'balanceOf':
28 | if len(args) == 1:
29 | return Get(ctx, args[0])
30 |
31 | elif operation == 'transfer':
32 | if len(args) == 3:
33 | return do_transfer(ctx, args[0], args[1], args[2])
34 |
35 | elif operation == 'transferFrom':
36 | if len(args) == 3:
37 | return do_transfer_from(ctx, args[0], args[1], args[2])
38 |
39 | elif operation == 'approve':
40 | if len(args) == 3:
41 | return do_approve(ctx, args[0], args[1], args[2])
42 |
43 | elif operation == 'allowance':
44 | if len(args) == 2:
45 | return do_allowance(ctx, args[0], args[1])
46 |
47 | return False
48 |
49 |
50 | def do_transfer(ctx, t_from, t_to, amount):
51 |
52 | if amount <= 0:
53 | return False
54 |
55 | if len(t_to) != 20:
56 | return False
57 |
58 | if CheckWitness(t_from):
59 |
60 | from_val = Get(ctx, t_from)
61 |
62 | if from_val < amount:
63 | print("insufficient funds")
64 | return False
65 |
66 | if t_from == t_to:
67 | print("transfer to self!")
68 | return True
69 |
70 | if from_val == amount:
71 | Delete(ctx, t_from)
72 |
73 | else:
74 | difference = from_val - amount
75 | Put(ctx, t_from, difference)
76 |
77 | to_value = Get(ctx, t_to)
78 |
79 | to_total = to_value + amount
80 |
81 | Put(ctx, t_to, to_total)
82 |
83 | OnTransfer(t_from, t_to, amount)
84 |
85 | return True
86 | else:
87 | print("from address is not the tx sender")
88 |
89 | return False
90 |
91 |
92 | def do_transfer_from(ctx, t_from, t_to, amount):
93 |
94 | if amount <= 0:
95 | return False
96 |
97 | available_key = concat(t_from, t_to)
98 |
99 | if len(available_key) != 40:
100 | return False
101 |
102 | available_to_to_addr = Get(ctx, available_key)
103 |
104 | if available_to_to_addr < amount:
105 | print("Insufficient funds approved")
106 | return False
107 |
108 | from_balance = Get(ctx, t_from)
109 |
110 | if from_balance < amount:
111 | print("Insufficient tokens in from balance")
112 | return False
113 |
114 | to_balance = Get(ctx, t_to)
115 |
116 | new_from_balance = from_balance - amount
117 |
118 | new_to_balance = to_balance + amount
119 |
120 | Put(ctx, t_to, new_to_balance)
121 | Put(ctx, t_from, new_from_balance)
122 |
123 | print("transfer complete")
124 |
125 | new_allowance = available_to_to_addr - amount
126 |
127 | if new_allowance == 0:
128 | print("removing all balance")
129 | Delete(ctx, available_key)
130 | else:
131 | print("updating allowance to new allowance")
132 | Put(ctx, available_key, new_allowance)
133 |
134 | OnTransfer(t_from, t_to, amount)
135 |
136 | return True
137 |
138 |
139 | def do_approve(ctx, t_owner, t_spender, amount):
140 |
141 | if len(t_spender) != 20:
142 | return False
143 |
144 | if not CheckWitness(t_owner):
145 | return False
146 |
147 | if amount < 0:
148 | return False
149 |
150 | # cannot approve an amount that is
151 | # currently greater than the from balance
152 | if Get(ctx, t_owner) >= amount:
153 |
154 | approval_key = concat(t_owner, t_spender)
155 |
156 | if amount == 0:
157 | Delete(ctx, approval_key)
158 | else:
159 | Put(ctx, approval_key, amount)
160 |
161 | OnApprove(t_owner, t_spender, amount)
162 |
163 | return True
164 |
165 | return False
166 |
167 |
168 | def do_allowance(ctx, t_owner, t_spender):
169 |
170 | return Get(ctx, concat(t_owner, t_spender))
171 |
--------------------------------------------------------------------------------
/nex/token.py:
--------------------------------------------------------------------------------
1 | """
2 | Basic settings for an NEP5 Token and crowdsale
3 | """
4 |
5 | from boa.interop.Neo.Storage import *
6 |
7 | TOKEN_NAME = 'NEX Template V2'
8 |
9 | TOKEN_SYMBOL = 'NXT2'
10 |
11 | TOKEN_DECIMALS = 8
12 |
13 | # This is the script hash of the address for the owner of the token
14 | # This can be found in ``neo-python`` with the walet open, use ``wallet`` command
15 | TOKEN_OWNER = b'S\xefB\xc8\xdf!^\xbeZ|z\xe8\x01\xcb\xc3\xac/\xacI)'
16 |
17 | TOKEN_CIRC_KEY = b'in_circulation'
18 |
19 | TOKEN_TOTAL_SUPPLY = 10000000 * 100000000 # 10m total supply * 10^8 ( decimals)
20 |
21 | TOKEN_INITIAL_AMOUNT = 2500000 * 100000000 # 2.5m to owners * 10^8
22 |
23 | # for now assume 1 dollar per token, and one neo = 40 dollars * 10^8
24 | TOKENS_PER_NEO = 40 * 100000000
25 |
26 | # for now assume 1 dollar per token, and one gas = 20 dollars * 10^8
27 | TOKENS_PER_GAS = 20 * 100000000
28 |
29 | # maximum amount you can mint in the limited round ( 500 neo/person * 40 Tokens/NEO * 10^8 )
30 | MAX_EXCHANGE_LIMITED_ROUND = 500 * 40 * 100000000
31 |
32 | # when to start the crowdsale
33 | BLOCK_SALE_START = 755000
34 |
35 | # when to end the initial limited round
36 | LIMITED_ROUND_END = 755000 + 10000
37 |
38 | KYC_KEY = b'kyc_ok'
39 |
40 | LIMITED_ROUND_KEY = b'r1'
41 |
42 |
43 | def crowdsale_available_amount(ctx):
44 | """
45 |
46 | :return: int The amount of tokens left for sale in the crowdsale
47 | """
48 |
49 | in_circ = Get(ctx, TOKEN_CIRC_KEY)
50 |
51 | available = TOKEN_TOTAL_SUPPLY - in_circ
52 |
53 | return available
54 |
55 |
56 | def add_to_circulation(ctx, amount):
57 | """
58 | Adds an amount of token to circlulation
59 |
60 | :param amount: int the amount to add to circulation
61 | """
62 |
63 | current_supply = Get(ctx, TOKEN_CIRC_KEY)
64 |
65 | current_supply += amount
66 | Put(ctx, TOKEN_CIRC_KEY, current_supply)
67 | return True
68 |
69 |
70 | def get_circulation(ctx):
71 | """
72 | Get the total amount of tokens in circulation
73 |
74 | :return:
75 | int: Total amount in circulation
76 | """
77 | return Get(ctx, TOKEN_CIRC_KEY)
78 |
--------------------------------------------------------------------------------
/nex/txio.py:
--------------------------------------------------------------------------------
1 | from boa.interop.System.ExecutionEngine import GetScriptContainer, GetExecutingScriptHash
2 | from boa.interop.Neo.Transaction import Transaction, GetReferences, GetOutputs, GetUnspentCoins
3 | from boa.interop.Neo.Output import GetValue, GetAssetId, GetScriptHash
4 |
5 |
6 | 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'
7 |
8 | gas_asset_id = b'\xe7-(iy\xeel\xb1\xb7\xe6]\xfd\xdf\xb2\xe3\x84\x10\x0b\x8d\x14\x8ewX\xdeB\xe4\x16\x8bqy,`'
9 |
10 |
11 | def get_asset_attachments():
12 | """
13 | Gets information about NEO and Gas attached to an invocation TX
14 |
15 | :return:
16 | list: A list with information about attached neo and gas
17 | """
18 |
19 | tx = GetScriptContainer()
20 | references = tx.References
21 |
22 | receiver_addr = GetExecutingScriptHash()
23 | sender_addr = None
24 | sent_amount_neo = 0
25 | sent_amount_gas = 0
26 |
27 | if len(references) > 0:
28 |
29 | reference = references[0]
30 | sender_addr = reference.ScriptHash
31 | for output in tx.Outputs:
32 | if output.ScriptHash == receiver_addr:
33 | if output.AssetId == neo_asset_id:
34 | sent_amount_neo += output.Value
35 | if output.AssetId == gas_asset_id:
36 | sent_amount_gas += output.Value
37 |
38 | return [receiver_addr, sender_addr, sent_amount_neo, sent_amount_gas]
39 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | autopep8==1.3.3
2 | bytecode==0.5
3 | neo-boa==0.3.7
4 | pycodestyle==2.3.1
5 |
--------------------------------------------------------------------------------
/requirements_test.txt:
--------------------------------------------------------------------------------
1 | asn1crypto==0.24.0
2 | attrs==17.4.0
3 | Automat==0.6.0
4 | autopep8==1.3.4
5 | base58==0.2.5
6 | bitcoin==1.1.42
7 | blessings==1.6.1
8 | bpython==0.17.1
9 | bumpversion==0.5.3
10 | bytecode==0.5
11 | certifi==2018.1.18
12 | cffi==1.11.4
13 | chardet==3.0.4
14 | colorlog==3.1.2
15 | constantly==15.1.0
16 | coverage==4.5.1
17 | coveralls==1.2.0
18 | cryptography==2.1.4
19 | curtsies==0.3.0
20 | cycler==0.10.0
21 | docopt==0.6.2
22 | ecdsa==0.13
23 | Events==0.3
24 | gevent==1.2.2
25 | greenlet==0.4.13
26 | hyperlink==18.0.0
27 | idna==2.6
28 | incremental==17.5.0
29 | klein==17.10.0
30 | logzero==1.3.1
31 | memory-profiler==0.52.0
32 | mmh3==2.5.1
33 | mock==2.0.0
34 | mpmath==1.0.0
35 | neo-boa==0.3.7
36 | -e git+https://github.com/CityOfZion/neo-python.git@99727d2622198c5bb1e64868780482e5b5dd043d#egg=neo_python
37 | neo-python-rpc==0.1.8
38 | neocore==0.3.6
39 | numpy==1.14.1
40 | pbr==3.1.1
41 | peewee==2.10.2
42 | pluggy==0.6.0
43 | plyvel==1.0.4
44 | prompt-toolkit==1.0.15
45 | psutil==5.4.3
46 | py==1.5.2
47 | pycodestyle==2.3.1
48 | pycparser==2.18
49 | pycrypto==2.6.1
50 | Pygments==2.2.0
51 | pymitter==0.2.3
52 | Pympler==0.5
53 | pyparsing==2.2.0
54 | python-dateutil==2.6.1
55 | pytz==2018.3
56 | requests==2.18.4
57 | scrypt==0.8.6
58 | six==1.11.0
59 | tqdm==4.19.5
60 | Twisted==17.9.0
61 | typing==3.6.4
62 | urllib3==1.22
63 | virtualenv==15.1.0
64 | wcwidth==0.1.7
65 | Werkzeug==0.14.1
66 | zope.interface==4.4.3
67 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pycodestyle]
2 | ignore = E721,E402,W291,E741
3 | max-line-length=256
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Always prefer setuptools over distutils
4 | from setuptools import setup, find_packages
5 | # To use a consistent encoding
6 | from codecs import open
7 | from os import path
8 |
9 | here = path.abspath(path.dirname(__file__))
10 |
11 | # Get the long description from the README file
12 | with open(path.join(here, 'README.md'), encoding='utf-8') as f:
13 | long_description = f.read()
14 |
15 |
16 | setup(
17 | name='neo-ico-template',
18 |
19 | # Versions should comply with PEP440. For a discussion on single-sourcing
20 | # the version across setup.py and the project code, see
21 | # https://packaging.python.org/en/latest/single_source_version.html
22 | version='0.2.0',
23 |
24 | description='A Python Template for an NEP5 Token',
25 | long_description=long_description,
26 |
27 | # The project's main homepage.
28 | url='https://github.com/neonexchange/neo-ico-template',
29 |
30 | # Author details
31 | author='Thomas Saunders',
32 | author_email='tom@neonexchange.org',
33 |
34 | # Choose your license
35 | license='GPL3',
36 |
37 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
38 | classifiers=[
39 | # How mature is this project? Common values are
40 | # 3 - Alpha
41 | # 4 - Beta
42 | # 5 - Production/Stable
43 | 'Development Status :: 3 - Alpha',
44 |
45 | # Indicate who your project is intended for
46 | 'Intended Audience :: Developers',
47 | 'Topic :: Software Development :: Build Tools',
48 |
49 | # Pick your license as you wish (should match "license" above)
50 | 'License :: OSI Approved :: MIT License',
51 |
52 | # Specify the Python versions you support here. In particular, ensure
53 | # that you indicate whether you support Python 2, Python 3 or both.
54 | 'Programming Language :: Python :: 3.6',
55 | 'Programming Language :: Python :: 3.7',
56 | ],
57 |
58 | # What does your project relate to?
59 | keywords='NEP5 ICO Token NEO .avm blockchain development dApp',
60 |
61 | # You can just specify the packages manually here if your project is
62 | # simple. Or you can use find_packages().
63 | # packages=find_packages(exclude=['contrib', 'docs', 'tests']),
64 |
65 | # Alternatively, if you want to distribute just a my_module.py, uncomment
66 | # this:
67 | py_modules=["nex"],
68 |
69 | # List run-time dependencies here. These will be installed by pip when
70 | # your project is installed. For an analysis of "install_requires" vs pip's
71 | # requirements files see:
72 | # https://packaging.python.org/en/latest/requirements.html
73 | install_requires=['neo-boa',],
74 |
75 |
76 | python_requires='>=3.4, <3.6',
77 |
78 | # List additional groups of dependencies here (e.g. development
79 | # dependencies). You can install these using the following syntax,
80 | # for example:
81 | # $ pip install -e .[dev,test]
82 | extras_require={
83 | 'dev': ['twine','wheel','sphinx','autopep8','pep8','sphinx-rtd-theme'],
84 | 'test': ['coverage'],
85 | },
86 |
87 | # If there are data files included in your packages that need to be
88 | # installed, specify them here. If using Python 2.6 or less, then these
89 | # have to be included in MANIFEST.in as well.
90 | #package_data={
91 | # 'sample': ['package_data.dat'],
92 | #},
93 |
94 | # Although 'package_data' is the preferred approach, in some case you may
95 | # need to place data files outside of your packages. See:
96 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
97 | # In this case, 'data_file' will be installed into '/my_data'
98 | #data_files=[('my_data', ['data/data_file'])],
99 |
100 | # To provide executable scripts, use entry points in preference to the
101 | # "scripts" keyword. Entry points provide cross-platform support and allow
102 | # pip to create the appropriate form of executable for the target platform.
103 | #entry_points={
104 | # 'console_scripts': [
105 | # 'sample=sample:main',
106 | # ],
107 | #},
108 | )
--------------------------------------------------------------------------------
/tests/test_ico_template.py:
--------------------------------------------------------------------------------
1 | from boa_test.tests.boa_test import BoaFixtureTest
2 | from boa.compiler import Compiler
3 | from neo.Core.TX.Transaction import Transaction
4 | from neo.Prompt.Commands.BuildNRun import TestBuild
5 | from neo.EventHub import events
6 | from neo.SmartContract.SmartContractEvent import SmartContractEvent, NotifyEvent
7 | from neo.Settings import settings
8 | from neo.Prompt.Utils import parse_param
9 | from neo.Core.FunctionCode import FunctionCode
10 | from neocore.Fixed8 import Fixed8
11 | from boa_test.example.demo.nex.token import *
12 |
13 | import shutil
14 | import os
15 |
16 | settings.USE_DEBUG_STORAGE = True
17 | settings.DEBUG_STORAGE_PATH = './fixtures/debugstorage'
18 |
19 |
20 | class TestContract(BoaFixtureTest):
21 |
22 | dispatched_events = []
23 | dispatched_logs = []
24 |
25 | @classmethod
26 | def tearDownClass(cls):
27 | super(BoaFixtureTest, cls).tearDownClass()
28 |
29 | try:
30 | if os.path.exists(settings.DEBUG_STORAGE_PATH):
31 | shutil.rmtree(settings.DEBUG_STORAGE_PATH)
32 | except Exception as e:
33 | print("couldn't remove debug storage %s " % e)
34 |
35 | @classmethod
36 | def setUpClass(cls):
37 | super(TestContract, cls).setUpClass()
38 |
39 | cls.dirname = '/'.join(os.path.abspath(__file__).split('/')[:-2])
40 |
41 | def on_notif(evt):
42 | print(evt)
43 | cls.dispatched_events.append(evt)
44 | print("dispatched events %s " % cls.dispatched_events)
45 |
46 | def on_log(evt):
47 | print(evt)
48 | cls.dispatched_logs.append(evt)
49 | events.on(SmartContractEvent.RUNTIME_NOTIFY, on_notif)
50 | events.on(SmartContractEvent.RUNTIME_LOG, on_log)
51 |
52 | def test_ICOTemplate_1(self):
53 |
54 | output = Compiler.instance().load('%s/ico_template.py' % TestContract.dirname).default
55 | out = output.write()
56 | # print(output.to_s())
57 |
58 | tx, results, total_ops, engine = TestBuild(out, ['name', '[]'], self.GetWallet1(), '0705', '05')
59 | self.assertEqual(len(results), 1)
60 | self.assertEqual(results[0].GetString(), TOKEN_NAME)
61 |
62 | tx, results, total_ops, engine = TestBuild(out, ['symbol', '[]'], self.GetWallet1(), '0705', '05')
63 | self.assertEqual(len(results), 1)
64 | self.assertEqual(results[0].GetString(), TOKEN_SYMBOL)
65 |
66 | tx, results, total_ops, engine = TestBuild(out, ['decimals', '[]'], self.GetWallet1(), '0705', '05')
67 | self.assertEqual(len(results), 1)
68 | self.assertEqual(results[0].GetBigInteger(), TOKEN_DECIMALS)
69 |
70 | tx, results, total_ops, engine = TestBuild(out, ['totalSupply', '[]'], self.GetWallet1(), '0705', '05')
71 | self.assertEqual(len(results), 1)
72 | self.assertEqual(results[0].GetBigInteger(), 0)
73 |
74 | tx, results, total_ops, engine = TestBuild(out, ['nonexistentmethod', '[]'], self.GetWallet1(), '0705', '05')
75 | self.assertEqual(len(results), 1)
76 | self.assertEqual(results[0].GetString(), 'unknown operation')
77 |
78 | # deploy with wallet 2 should fail CheckWitness
79 | tx, results, total_ops, engine = TestBuild(out, ['deploy', '[]'], self.GetWallet2(), '0705', '05')
80 | self.assertEqual(len(results), 1)
81 | self.assertEqual(results[0].GetBoolean(), False)
82 |
83 | tx, results, total_ops, engine = TestBuild(out, ['deploy', '[]'], self.GetWallet1(), '0705', '05')
84 | self.assertEqual(len(results), 1)
85 | self.assertEqual(results[0].GetBoolean(), True)
86 |
87 | # second time, it should already be deployed and return false
88 | tx, results, total_ops, engine = TestBuild(out, ['deploy', '[]'], self.GetWallet1(), '0705', '05')
89 | self.assertEqual(len(results), 1)
90 | self.assertEqual(results[0].GetBoolean(), False)
91 |
92 | # now total supply should be equal to the initial owner amount
93 | tx, results, total_ops, engine = TestBuild(out, ['totalSupply', '[]'], self.GetWallet1(), '0705', '05')
94 | self.assertEqual(len(results), 1)
95 | self.assertEqual(results[0].GetBigInteger(), TOKEN_INITIAL_AMOUNT)
96 |
97 | # now the owner should have a balance of the TOKEN_INITIAL_AMOUNT
98 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param([bytearray(TOKEN_OWNER)])], self.GetWallet1(), '0705', '05')
99 | self.assertEqual(len(results), 1)
100 | self.assertEqual(results[0].GetBigInteger(), TOKEN_INITIAL_AMOUNT)
101 |
102 | def test_ICOTemplate_2(self):
103 |
104 | output = Compiler.instance().load('%s/ico_template.py' % TestContract.dirname).default
105 | out = output.write()
106 |
107 | # now transfer tokens to wallet 2
108 |
109 | TestContract.dispatched_events = []
110 |
111 | test_transfer_amount = 2400000001
112 | tx, results, total_ops, engine = TestBuild(out, ['transfer', parse_param([bytearray(TOKEN_OWNER), self.wallet_2_script_hash.Data, test_transfer_amount])], self.GetWallet1(), '0705', '05')
113 | self.assertEqual(len(results), 1)
114 | self.assertEqual(results[0].GetBoolean(), True)
115 |
116 | self.assertEqual(len(TestContract.dispatched_events), 1)
117 | evt = TestContract.dispatched_events[0]
118 | self.assertIsInstance(evt, NotifyEvent)
119 | self.assertEqual(evt.addr_from.Data, bytearray(TOKEN_OWNER))
120 | self.assertEqual(evt.addr_to, self.wallet_2_script_hash)
121 | self.assertEqual(evt.amount, test_transfer_amount)
122 |
123 | # now get balance of wallet 2
124 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param([self.wallet_2_script_hash.Data])], self.GetWallet1(), '0705', '05')
125 | self.assertEqual(len(results), 1)
126 | self.assertEqual(results[0].GetBigInteger(), test_transfer_amount)
127 |
128 | # now the owner should have less
129 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param([bytearray(TOKEN_OWNER)])], self.GetWallet1(), '0705', '05')
130 | self.assertEqual(len(results), 1)
131 | self.assertEqual(results[0].GetBigInteger(), TOKEN_INITIAL_AMOUNT - test_transfer_amount)
132 |
133 | # now this transfer should fail
134 | tx, results, total_ops, engine = TestBuild(out, ['transfer', parse_param([bytearray(TOKEN_OWNER), self.wallet_2_script_hash.Data, TOKEN_INITIAL_AMOUNT])], self.GetWallet1(), '0705', '05')
135 | self.assertEqual(len(results), 1)
136 | self.assertEqual(results[0].GetBoolean(), False)
137 |
138 | # this transfer should fail because it is not signed by the 'from' address
139 | tx, results, total_ops, engine = TestBuild(out, ['transfer', parse_param([bytearray(TOKEN_OWNER), self.wallet_2_script_hash.Data, 10000])], self.GetWallet3(), '0705', '05')
140 | self.assertEqual(len(results), 1)
141 | self.assertEqual(results[0].GetBoolean(), False)
142 |
143 | # now this transfer should fail, this is from address with no tokens
144 | tx, results, total_ops, engine = TestBuild(out, ['transfer', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 1000])], self.GetWallet3(), '0705', '05')
145 | self.assertEqual(len(results), 1)
146 | self.assertEqual(results[0].GetBoolean(), False)
147 |
148 | # get balance of bad data
149 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param(['abc'])], self.GetWallet1(), '0705', '05')
150 | self.assertEqual(len(results), 1)
151 | self.assertEqual(results[0].GetBigInteger(), 0)
152 |
153 | # get balance no params
154 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param([])], self.GetWallet1(), '0705', '05')
155 | self.assertEqual(len(results), 1)
156 | self.assertEqual(results[0].GetBoolean(), False)
157 |
158 | def test_ICOTemplate_3_KYC(self):
159 |
160 | output = Compiler.instance().load('%s/ico_template.py' % TestContract.dirname).default
161 | out = output.write()
162 | print(output.to_s())
163 | # now transfer tokens to wallet 2
164 |
165 | TestContract.dispatched_events = []
166 |
167 | # test mint tokens without being kyc verified
168 | tx, results, total_ops, engine = TestBuild(out, ['mintTokens', '[]', '--attach-neo=10'], self.GetWallet3(), '0705', '05')
169 | self.assertEqual(len(results), 1)
170 | self.assertEqual(results[0].GetBoolean(), False)
171 |
172 | # Try to register as a non owner
173 | tx, results, total_ops, engine = TestBuild(out, ['crowdsale_register', parse_param([self.wallet_3_script_hash.Data])], self.GetWallet3(), '0705', '05')
174 | self.assertEqual(len(results), 1)
175 | self.assertEqual(results[0].GetBoolean(), False)
176 |
177 | # Get status of non registered address
178 | tx, results, total_ops, engine = TestBuild(out, ['crowdsale_status', parse_param([self.wallet_3_script_hash.Data])], self.GetWallet3(), '0705', '05')
179 | self.assertEqual(len(results), 1)
180 | self.assertEqual(results[0].GetBoolean(), False)
181 |
182 | TestContract.dispatched_events = []
183 |
184 | # register an address
185 | tx, results, total_ops, engine = TestBuild(out, ['crowdsale_register', parse_param([self.wallet_3_script_hash.Data])], self.GetWallet1(), '0705', '05')
186 | self.assertEqual(len(results), 1)
187 | self.assertEqual(results[0].GetBigInteger(), 1)
188 |
189 | # it should dispatch an event
190 | self.assertEqual(len(TestContract.dispatched_events), 1)
191 | evt = TestContract.dispatched_events[0]
192 | self.assertEqual(evt.event_payload[0], b'kyc_registration')
193 |
194 | # register 2 addresses at once
195 | tx, results, total_ops, engine = TestBuild(out, ['crowdsale_register', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data])], self.GetWallet1(), '0705', '05')
196 | self.assertEqual(len(results), 1)
197 | self.assertEqual(results[0].GetBigInteger(), 2)
198 |
199 | # now check reg status
200 | tx, results, total_ops, engine = TestBuild(out, ['crowdsale_status', parse_param([self.wallet_3_script_hash.Data])], self.GetWallet3(), '0705', '05')
201 | self.assertEqual(len(results), 1)
202 | self.assertEqual(results[0].GetBoolean(), True)
203 |
204 | def test_ICOTemplate_4_attachments(self):
205 |
206 | output = Compiler.instance().load('%s/ico_template.py' % TestContract.dirname).default
207 | out = output.write()
208 |
209 | # test mint tokens without being kyc verified
210 | tx, results, total_ops, engine = TestBuild(out, ['get_attachments', '[]', '--attach-neo=10'], self.GetWallet3(), '0705', '05')
211 | self.assertEqual(len(results), 1)
212 | attachments = results[0].GetArray()
213 | self.assertEqual(len(attachments), 4)
214 |
215 | fn = FunctionCode(out, '0705', '05')
216 |
217 | self.assertEqual(attachments[0].GetByteArray(), fn.ScriptHash().Data)
218 | self.assertEqual(attachments[1].GetByteArray(), self.wallet_3_script_hash.Data)
219 | self.assertEqual(attachments[2].GetBigInteger(), Fixed8.FromDecimal(10).value)
220 | self.assertEqual(attachments[3].GetBigInteger(), 0)
221 |
222 | tx, results, total_ops, engine = TestBuild(out, ['get_attachments', '[]'], self.GetWallet3(), '0705', '05')
223 | self.assertEqual(len(results), 1)
224 | attachments = results[0].GetArray()
225 | self.assertEqual(len(attachments), 4)
226 |
227 | self.assertEqual(attachments[1].GetByteArray(), bytearray())
228 | self.assertEqual(attachments[2].GetBigInteger(), 0)
229 | self.assertEqual(attachments[3].GetBigInteger(), 0)
230 |
231 | tx, results, total_ops, engine = TestBuild(out, ['get_attachments', '[]', '--attach-neo=3', '--attach-gas=3.12'], self.GetWallet1(), '0705', '05')
232 | self.assertEqual(len(results), 1)
233 | attachments = results[0].GetArray()
234 | self.assertEqual(len(attachments), 4)
235 | self.assertEqual(attachments[1].GetByteArray(), self.wallet_1_script_hash.Data)
236 | self.assertEqual(attachments[2].GetBigInteger(), Fixed8.FromDecimal(3).value)
237 | self.assertEqual(attachments[3].GetBigInteger(), Fixed8.FromDecimal(3.12).value)
238 |
239 | def test_ICOTemplate_5_mint(self):
240 |
241 | output = Compiler.instance().load('%s/ico_template.py' % TestContract.dirname).default
242 | out = output.write()
243 |
244 | # register an address
245 | tx, results, total_ops, engine = TestBuild(out, ['crowdsale_register', parse_param([self.wallet_3_script_hash.Data])], self.GetWallet1(), '0705', '05')
246 | self.assertEqual(len(results), 1)
247 | self.assertEqual(results[0].GetBigInteger(), 1)
248 |
249 | TestContract.dispatched_events = []
250 |
251 | # test mint tokens, this should return true
252 | tx, results, total_ops, engine = TestBuild(out, ['mintTokens', '[]', '--attach-neo=10'], self.GetWallet3(), '0705', '05')
253 | self.assertEqual(len(results), 1)
254 | self.assertEqual(results[0].GetBoolean(), True)
255 |
256 | # it should dispatch an event
257 | self.assertEqual(len(TestContract.dispatched_events), 1)
258 | evt = TestContract.dispatched_events[0]
259 | self.assertIsInstance(evt, NotifyEvent)
260 | self.assertEqual(evt.amount, 10 * TOKENS_PER_NEO)
261 | self.assertEqual(evt.addr_to, self.wallet_3_script_hash)
262 |
263 | # test mint tokens again, this should be false since you can't do it twice
264 | tx, results, total_ops, engine = TestBuild(out, ['mintTokens', '[]', '--attach-neo=10'], self.GetWallet3(), '0705', '05')
265 | self.assertEqual(len(results), 1)
266 | self.assertEqual(results[0].GetBoolean(), False)
267 |
268 | # now the minter should have a balance
269 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param([self.wallet_3_script_hash.Data])], self.GetWallet1(), '0705', '05')
270 | self.assertEqual(len(results), 1)
271 | self.assertEqual(results[0].GetBigInteger(), 10 * TOKENS_PER_NEO)
272 |
273 | # now the total circulation should be bigger
274 | tx, results, total_ops, engine = TestBuild(out, ['totalSupply', '[]'], self.GetWallet1(), '0705', '05')
275 | self.assertEqual(len(results), 1)
276 | self.assertEqual(results[0].GetBigInteger(), (10 * TOKENS_PER_NEO) + TOKEN_INITIAL_AMOUNT)
277 |
278 | def test_ICOTemplate_6_approval(self):
279 |
280 | output = Compiler.instance().load('%s/ico_template.py' % TestContract.dirname).default
281 | out = output.write()
282 |
283 | # tranfer_from, approve, allowance
284 | tx, results, total_ops, engine = TestBuild(out, ['allowance', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data])], self.GetWallet2(), '0705', '05')
285 | self.assertEqual(len(results), 1)
286 | self.assertEqual(results[0].GetBigInteger(), 0)
287 |
288 | # try to transfer from
289 | tx, results, total_ops, engine = TestBuild(out, ['transferFrom', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 10000])], self.GetWallet2(), '0705', '05')
290 | self.assertEqual(len(results), 1)
291 | self.assertEqual(results[0].GetBoolean(), False)
292 |
293 | # try to approve from someone not yourself
294 | tx, results, total_ops, engine = TestBuild(out, ['approve', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 10000])], self.GetWallet2(), '0705', '05')
295 | self.assertEqual(len(results), 1)
296 | self.assertEqual(results[0].GetBigInteger(), 0)
297 |
298 | # try to approve more than you have
299 | tx, results, total_ops, engine = TestBuild(out, ['approve', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, TOKEN_INITIAL_AMOUNT])], self.GetWallet3(), '0705', '05')
300 | self.assertEqual(len(results), 1)
301 | self.assertEqual(results[0].GetBigInteger(), 0)
302 |
303 | TestContract.dispatched_events = []
304 |
305 | # approve should work
306 | tx, results, total_ops, engine = TestBuild(out, ['approve', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 1234])], self.GetWallet3(), '0705', '05')
307 | self.assertEqual(len(results), 1)
308 | self.assertEqual(results[0].GetBoolean(), True)
309 |
310 | # it should dispatch an event
311 | self.assertEqual(len(TestContract.dispatched_events), 1)
312 | evt = TestContract.dispatched_events[0]
313 | self.assertIsInstance(evt, NotifyEvent)
314 | self.assertEqual(evt.notify_type, b'approve')
315 | self.assertEqual(evt.amount, 1234)
316 |
317 | # check allowance
318 | tx, results, total_ops, engine = TestBuild(out, ['allowance', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data])], self.GetWallet2(), '0705', '05')
319 | self.assertEqual(len(results), 1)
320 | self.assertEqual(results[0].GetBigInteger(), 1234)
321 |
322 | # approve should not be additive, it should overwrite previous approvals
323 | tx, results, total_ops, engine = TestBuild(out, ['approve', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 133234])], self.GetWallet3(), '0705', '05')
324 | self.assertEqual(len(results), 1)
325 | self.assertEqual(results[0].GetBoolean(), True)
326 |
327 | tx, results, total_ops, engine = TestBuild(out, ['allowance', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data])], self.GetWallet2(), '0705', '05')
328 | self.assertEqual(len(results), 1)
329 | self.assertEqual(results[0].GetBigInteger(), 133234)
330 |
331 | # now you can transfer from
332 | tx, results, total_ops, engine = TestBuild(out, ['transferFrom', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 10000])], self.GetWallet2(), '0705', '05')
333 | self.assertEqual(len(results), 1)
334 | self.assertEqual(results[0].GetBoolean(), True)
335 |
336 | # now the recevier should have a balance
337 | # it is equal to 10000 plus test_transfer_amount = 2400000001
338 |
339 | tx, results, total_ops, engine = TestBuild(out, ['balanceOf', parse_param([self.wallet_2_script_hash.Data])], self.GetWallet1(), '0705', '05')
340 | self.assertEqual(len(results), 1)
341 | self.assertEqual(results[0].GetBigInteger(), 10000 + 2400000001)
342 |
343 | # now the allowance should be less
344 | tx, results, total_ops, engine = TestBuild(out, ['allowance', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data])], self.GetWallet2(), '0705', '05')
345 | self.assertEqual(len(results), 1)
346 | self.assertEqual(results[0].GetBigInteger(), 133234 - 10000)
347 |
348 | # try to transfer too much, even with approval
349 | tx, results, total_ops, engine = TestBuild(out, ['transferFrom', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, 14440000])], self.GetWallet2(), '0705', '05')
350 | self.assertEqual(len(results), 1)
351 | self.assertEqual(results[0].GetBoolean(), False)
352 |
353 | # cant approve negative amounts
354 | tx, results, total_ops, engine = TestBuild(out, ['approve', parse_param([self.wallet_3_script_hash.Data, self.wallet_2_script_hash.Data, -1000])], self.GetWallet3(), '0705', '05')
355 | self.assertEqual(len(results), 1)
356 | self.assertEqual(results[0].GetBoolean(), False)
357 |
--------------------------------------------------------------------------------