├── node.json ├── requirements.txt ├── Makefile ├── tests ├── requirements.txt ├── utils.py └── test_AccessController.py ├── .gitignore ├── contracts ├── AccessController.cairo └── libraries │ └── AccessController_base.cairo ├── README.md └── LICENSE /node.json: -------------------------------------------------------------------------------- 1 | {"localhost": "http://localhost:5000/"} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openzeppelin-cairo-contracts 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Build and test 2 | build :; nile compile 3 | test :; pytest tests/ 4 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==4.0.1 2 | pytest==7.1.2 3 | pytest-asyncio==0.18.3 4 | cairo-lang==0.8.1 5 | pytest-describe==2.0.1 6 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for testing Cairo contracts.""" 2 | 3 | from starkware.cairo.common.hash_state import compute_hash_on_elements 4 | from starkware.crypto.signature.signature import private_to_stark_key, sign 5 | from starkware.starknet.definitions.error_codes import StarknetErrorCode 6 | from starkware.starkware_utils.error_handling import StarkException 7 | from starkware.starknet.public.abi import get_selector_from_name 8 | 9 | MAX_UINT256 = (2**128 - 1, 2**128 - 1) 10 | 11 | 12 | def str_to_felt(text): 13 | b_text = bytes(text, 'UTF-8') 14 | return int.from_bytes(b_text, "big") 15 | 16 | 17 | def uint(a): 18 | return (a, 0) 19 | 20 | 21 | async def assert_revert(fun): 22 | try: 23 | await fun 24 | assert False 25 | except StarkException as err: 26 | _, error = err.args 27 | assert error['code'] == StarknetErrorCode.TRANSACTION_FAILED 28 | 29 | 30 | class Signer(): 31 | """ 32 | Utility for sending signed transactions to an Account on Starknet. 33 | 34 | Parameters 35 | ---------- 36 | 37 | private_key : int 38 | 39 | Examples 40 | --------- 41 | Constructing a Singer object 42 | 43 | >>> signer = Signer(1234) 44 | 45 | Sending a transaction 46 | 47 | >>> await signer.send_transaction(account, 48 | account.contract_address, 49 | 'set_public_key', 50 | [other.public_key] 51 | ) 52 | 53 | """ 54 | 55 | def __init__(self, private_key): 56 | self.private_key = private_key 57 | self.public_key = private_to_stark_key(private_key) 58 | 59 | def sign(self, message_hash): 60 | return sign(msg_hash=message_hash, priv_key=self.private_key) 61 | 62 | async def send_transaction(self, account, to, selector_name, calldata, nonce=None): 63 | if nonce is None: 64 | execution_info = await account.get_nonce().call() 65 | nonce, = execution_info.result 66 | 67 | selector = get_selector_from_name(selector_name) 68 | message_hash = hash_message(account.contract_address, to, selector, calldata, nonce) 69 | sig_r, sig_s = self.sign(message_hash) 70 | 71 | return await account.execute(to, selector, calldata, nonce).invoke(signature=[sig_r, sig_s]) 72 | 73 | 74 | def hash_message(sender, to, selector, calldata, nonce): 75 | message = [sender, to, selector, compute_hash_on_elements(calldata), nonce] 76 | return compute_hash_on_elements(message) 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | .env 3 | .idea/ 4 | *-env 5 | *accounts.json 6 | *deployments.txt 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | -------------------------------------------------------------------------------- /contracts/AccessController.cairo: -------------------------------------------------------------------------------- 1 | // Declare this file as a StarkNet contract. 2 | %lang starknet 3 | 4 | from starkware.cairo.common.cairo_builtins import HashBuiltin 5 | from starkware.starknet.common.syscalls import get_caller_address 6 | from openzeppelin.access.ownable.library import Ownable 7 | from contracts.libraries.AccessController_base import ( 8 | AccessController_initializer, 9 | AccessController_isAllowed, 10 | AccessController_freeSlotsCount, 11 | AccessController_increaseMaxSlots, 12 | AccessController_register, 13 | AccessController_forceRegister, 14 | AccessController_forceRegisterBatch, 15 | ) 16 | 17 | @constructor 18 | func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 19 | initial_allowed_access: felt, owner_address: felt 20 | ) { 21 | Ownable.initializer(owner_address); 22 | AccessController_initializer(initial_allowed_access); 23 | return (); 24 | } 25 | 26 | // 27 | // Getters 28 | // 29 | 30 | @view 31 | func isAllowed{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(address: felt) -> ( 32 | is_allowed: felt 33 | ) { 34 | let (is_allowed) = AccessController_isAllowed(address); 35 | return (is_allowed,); 36 | } 37 | 38 | @view 39 | func freeSlotsCount{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( 40 | free_slots_count: felt 41 | ) { 42 | let (free_slots_count) = AccessController_freeSlotsCount(); 43 | return (free_slots_count,); 44 | } 45 | 46 | @view 47 | func getOwner{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (owner: felt) { 48 | let (owner) = Ownable.owner(); 49 | return (owner=owner); 50 | } 51 | 52 | // 53 | // Externals 54 | // 55 | 56 | @external 57 | func increaseMaxSlots{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 58 | increase_max_slots_by: felt 59 | ) { 60 | // Ownable check in function 61 | AccessController_increaseMaxSlots(increase_max_slots_by); 62 | return (); 63 | } 64 | 65 | @external 66 | func register{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { 67 | // Anyone can register a new slot 68 | // Tx will fail if no more slots available 69 | let (caller_address) = get_caller_address(); 70 | AccessController_register(caller_address); 71 | return (); 72 | } 73 | 74 | @external 75 | func forceRegister{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(address: felt) { 76 | // Ownable check in function 77 | // Force the register, total count will be increase 78 | AccessController_forceRegister(address); 79 | return (); 80 | } 81 | 82 | @external 83 | func forceRegisterBatch{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 84 | batch_address_len: felt, batch_address: felt* 85 | ) { 86 | // Ownable check in function 87 | // Force the batch register, total count will be increase 88 | AccessController_forceRegisterBatch(batch_address_len, batch_address); 89 | return (); 90 | } 91 | 92 | @external 93 | func transferOwnership{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 94 | new_owner: felt 95 | ) -> (new_owner: felt) { 96 | // Ownable check in function 97 | Ownable.transfer_ownership(new_owner); 98 | return (new_owner=new_owner); 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Access Controller Contracts 2 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![version](https://img.shields.io/badge/version-1.0.0-blue) 3 | 4 | Simple on-chain access controller contract. This contract permit an on-chain registration mechanism with a concept of slots. 5 | 6 | Owner can specify a number of slots (`maxSlotsCount`). Everyone is able to `register` to get a slot. When the `slotUsedCount` eq the `maxSlotsCount` there is no more space to register. 7 | 8 | With this simple approach you can use an on-chain contract to manage accesses of your contracts or even of your frontends. This is a very useful tool when you want to give progessive access. 9 | 10 | 11 | ## Contracts 12 | 13 | The contract has three state variables: 14 | 15 | ```cairo 16 | @storage_var 17 | func AccessController_maxSlotsCount() -> (max: felt): 18 | end 19 | 20 | @storage_var 21 | func AccessController_slotUsedCount() -> (entries: felt): 22 | end 23 | 24 | @storage_var 25 | func AccessController_whitelist(address: felt) -> (whitelisted: felt): 26 | end 27 | ``` 28 | 29 | ### Deploy 30 | 31 | When deploying the contract you have to pass two args: 32 | 33 | ```cairo 34 | ( 35 | initial_allowed_access: felt, # Number of initial slots available 36 | owner_address: felt # Owner of the contract who will be able to increase # of slots 37 | ) 38 | ``` 39 | 40 | ### Management 41 | 42 | Once you deployed the contract, you can increase the number of maximum slots available. To do that make a transaction by invoking the `increaseMaxSlots` function from the owner wallet. The argument is `increase_max_slots_by` which is the number of slots you want to add. 43 | 44 | Other useful functions can be found [here](https://github.com/419Labs/access-controller-contracts/blob/update/docs/contracts/AccessController.cairo) 45 | 46 | 47 | ## Use in ReactJS 48 | 49 | Using this contract is deadsimple. First of all import ABI and create your `Contract` object: 50 | 51 | ```javascript 52 | import { Contract, json } from "starknet"; 53 | 54 | const compiledARFController = json.parse(JSON.stringify(arfControllerAbi)); 55 | arfControllerContract: new Contract( 56 | compiledARFController, 57 | CONTROLLER_CONTRACT_ADDRESS 58 | ); 59 | ``` 60 | 61 | Verify if an address is allowed/registered: 62 | 63 | ```javascript 64 | accessControllerContract 65 | .isAllowed("0x1234...6789") 66 | .then((response: CallContractResponse) => { 67 | // response.is_allowed 68 | }) 69 | ``` 70 | 71 | Permit a user to register in order to get a free slot: 72 | 73 | ```javascript 74 | accessControllerContract 75 | .invoke("register", []) 76 | .then((response: AddTransactionResponse) => { 77 | // Transaction added 78 | }).catch(() => { 79 | // Error 80 | }); 81 | ``` 82 | 83 | Check the number of available slots: 84 | 85 | ```javascript 86 | accessControllerContract 87 | .freeSlotsCount() 88 | .then((response: CallContractResponse) => { 89 | // response.free_slots_count 90 | }) 91 | ``` 92 | 93 | ## Use case 94 | 95 | This contract has been used for [Alpha Road](https://twitter.com/alpharoad_fi) during the first Testnet phases of the launch of our first offering: a one-click revisited AMM. 96 | 97 | ## Tests 98 | 99 | ### Run tests 100 | 101 | First, install requirements: 102 | 103 | ```sh 104 | pip install -r requirements.txt 105 | pip install -r tests/requirements.txt 106 | ``` 107 | 108 | Run all tests: 109 | 110 | ```sh 111 | pytest 112 | ``` 113 | 114 | Run a specific test: 115 | 116 | ```sh 117 | pytest tests/test_AccessController.py -k test_transfer_ownership_should_fail_when_caller_is_not_owner 118 | ``` 119 | 120 | ### Linter 121 | 122 | To make our tests readable we use a standard linter: [flake8](https://flake8.pycqa.org/en/latest/) 123 | 124 | Run linter: 125 | 126 | ```sh 127 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics 128 | ``` 129 | 130 | Flake will only act as linter and will not help you to fix/format your python files. We recommend using: [yapf](https://github.com/google/yapf) 131 | 132 | E.g: 133 | 134 | ```sh 135 | yapf --recursive --style='{based_on_style: pep8, column_limit: 120, indent_width: 4}' -i tests 136 | ``` 137 | 138 | ## Improvements 139 | 140 | Feel free to improve this by providing a PR. 141 | -------------------------------------------------------------------------------- /contracts/libraries/AccessController_base.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from starkware.cairo.common.cairo_builtins import HashBuiltin 4 | from starkware.starknet.common.syscalls import get_caller_address 5 | from starkware.cairo.common.math import assert_not_zero, assert_nn 6 | from openzeppelin.access.ownable.library import Ownable 7 | 8 | // 9 | // Events 10 | // 11 | 12 | @event 13 | func Register(registered_address: felt) { 14 | } 15 | 16 | @event 17 | func IncreaseMaxSlots(slots_added_count: felt) { 18 | } 19 | 20 | // 21 | // Storage 22 | // 23 | 24 | @storage_var 25 | func AccessController_maxSlotsCount() -> (max: felt) { 26 | } 27 | 28 | @storage_var 29 | func AccessController_slotUsedCount() -> (entries: felt) { 30 | } 31 | 32 | @storage_var 33 | func AccessController_whitelist(address: felt) -> (whitelisted: felt) { 34 | } 35 | 36 | func AccessController_initializer{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 37 | initial_allowed_access: felt 38 | ) { 39 | // Init max entries 40 | AccessController_maxSlotsCount.write(initial_allowed_access); 41 | // count is at 0 42 | return (); 43 | } 44 | 45 | // 46 | // Getters 47 | // 48 | 49 | func AccessController_isAllowed{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 50 | address: felt 51 | ) -> (is_allowed: felt) { 52 | // Check if an address is registered in the whitelist 53 | let (is_allowed) = AccessController_whitelist.read(address); 54 | return (is_allowed,); 55 | } 56 | 57 | // Return the current count of free slots 58 | func AccessController_freeSlotsCount{ 59 | syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr 60 | }() -> (free_slots_count: felt) { 61 | let (current_max_count) = AccessController_maxSlotsCount.read(); 62 | let (current_count) = AccessController_slotUsedCount.read(); 63 | let free_slots_count = current_max_count - current_count; 64 | return (free_slots_count,); 65 | } 66 | 67 | // 68 | // Externals 69 | // 70 | 71 | // Increase the total slots available 72 | func AccessController_increaseMaxSlots{ 73 | syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr 74 | }(increase_max_slots_by: felt) { 75 | // Only Owner should be able to increase the available slots 76 | Ownable.assert_only_owner(); 77 | // Check increase value is positive 78 | assert_nn(increase_max_slots_by); 79 | 80 | let (current_max_count) = AccessController_maxSlotsCount.read(); 81 | let new_max_count = current_max_count + increase_max_slots_by; 82 | AccessController_maxSlotsCount.write(new_max_count); 83 | 84 | // Emit slots increase event 85 | IncreaseMaxSlots.emit(slots_added_count=increase_max_slots_by); 86 | return (); 87 | } 88 | 89 | // Register a new whitlisted address if there is at least 1 free slot 90 | // Everybody should be able to register if there is a free slot 91 | func AccessController_register{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( 92 | address: felt 93 | ) { 94 | _register(address); 95 | return (); 96 | } 97 | 98 | // Register a new whitelisted address even if there is no more free slot 99 | // Only owner should be able to add it 100 | func AccessController_forceRegister{ 101 | syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr 102 | }(address: felt) { 103 | alloc_locals; 104 | 105 | // Only Owner should be able to force a register 106 | Ownable.assert_only_owner(); 107 | 108 | // If no free slot -> increase for 1 more 109 | let (free_slots_count) = AccessController_freeSlotsCount(); 110 | tempvar syscall_ptr = syscall_ptr; 111 | tempvar pedersen_ptr = pedersen_ptr; 112 | tempvar range_check_ptr = range_check_ptr; 113 | if (free_slots_count == 0) { 114 | AccessController_increaseMaxSlots(1); 115 | } 116 | 117 | // Register the new whitelisted address 118 | _register(address); 119 | return (); 120 | } 121 | 122 | func AccessController_forceRegisterBatch{ 123 | syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr 124 | }(batch_address_len: felt, batch_address: felt*) { 125 | // Only Owner should be able to force a register batch 126 | Ownable.assert_only_owner(); 127 | 128 | if (batch_address_len == 0) { 129 | return (); 130 | } 131 | 132 | let new_address_to_register = batch_address[0]; 133 | AccessController_forceRegister(new_address_to_register); 134 | 135 | return AccessController_forceRegisterBatch( 136 | batch_address_len - 1, batch_address=&batch_address[1] 137 | ); 138 | } 139 | 140 | // 141 | // Internals 142 | // 143 | 144 | func _register{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(address: felt) { 145 | let (free_slots_count) = AccessController_freeSlotsCount(); 146 | 147 | // Check there is free slots & address not null 148 | assert_not_zero(free_slots_count); 149 | assert_not_zero(address); 150 | 151 | // Check not already registered 152 | let (is_already_registered) = AccessController_whitelist.read(address); 153 | 154 | assert is_already_registered = 0; 155 | 156 | // Write address to whitelisted & increase total count 157 | AccessController_whitelist.write(address, 1); 158 | let (current_count) = AccessController_slotUsedCount.read(); 159 | AccessController_slotUsedCount.write(current_count + 1); 160 | 161 | // Emit registration event 162 | Register.emit(registered_address=address); 163 | return (); 164 | } 165 | -------------------------------------------------------------------------------- /tests/test_AccessController.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytest_asyncio 3 | from starkware.starknet.testing.contract import StarknetContract 4 | from starkware.starknet.testing.starknet import Starknet 5 | from starkware.starkware_utils.error_handling import StarkException 6 | from utils import Signer 7 | 8 | # Testing vars 9 | owner = Signer(123456789987654321) 10 | not_owner = Signer(111111111987654323) 11 | address1 = Signer(111111111987654324) 12 | address2 = Signer(111111111987654325) 13 | 14 | 15 | @pytest_asyncio.fixture 16 | async def starknet() -> Starknet: 17 | return await Starknet.empty() 18 | 19 | 20 | @pytest_asyncio.fixture 21 | async def contract(starknet: Starknet) -> StarknetContract: 22 | return await starknet.deploy("contracts/AccessController.cairo", constructor_calldata=[10, owner.public_key]) 23 | 24 | 25 | def describe_is_allowed(): 26 | 27 | @pytest.mark.asyncio 28 | async def it_should_return_false_when_address_is_not_in_whitelist(contract): 29 | # When 30 | execution = await contract.isAllowed(address1.public_key).call() 31 | 32 | # Then 33 | assert execution.result.is_allowed == 0 34 | 35 | 36 | def describe_free_slot_count(): 37 | 38 | @pytest.mark.asyncio 39 | async def it_should_return_remaining_slot_number(contract): 40 | # When 41 | execution = await contract.freeSlotsCount().call() 42 | 43 | # Then 44 | assert execution.result.free_slots_count == 10 45 | 46 | 47 | def describe_get_owner(): 48 | 49 | @pytest.mark.asyncio 50 | async def it_should_return_owner_public_key(contract): 51 | # When 52 | execution = await contract.getOwner().call() 53 | 54 | # Then 55 | assert execution.result.owner == owner.public_key 56 | 57 | 58 | def describe_increase_max_slot(): 59 | 60 | @pytest.mark.asyncio 61 | async def it_should_succeed_when_caller_is_owner(contract): 62 | # When 63 | await contract.increaseMaxSlots(2).invoke(caller_address=owner.public_key) 64 | 65 | # Then 66 | execution = await contract.freeSlotsCount().call() 67 | assert execution.result.free_slots_count == 12 68 | 69 | @pytest.mark.asyncio 70 | async def it_should_fail_when_caller_is_not_owner(contract): 71 | # When 72 | with pytest.raises(StarkException): 73 | await contract.increaseMaxSlots(2).invoke(caller_address=not_owner.public_key) 74 | 75 | # Then 76 | execution = await contract.freeSlotsCount().call() 77 | assert execution.result.free_slots_count == 10 78 | 79 | 80 | def describe_register(): 81 | 82 | @pytest.mark.asyncio 83 | async def it_should_add_address_to_whitelist(contract): 84 | # When 85 | await contract.register().invoke(caller_address=address1.public_key) 86 | 87 | # Then 88 | execution = await contract.isAllowed(address1.public_key).call() 89 | assert execution.result.is_allowed == 1 90 | execution = await contract.freeSlotsCount().call() 91 | assert execution.result.free_slots_count == 9 92 | 93 | 94 | def describe_force_register(): 95 | 96 | @pytest.mark.asyncio 97 | async def it_should_add_address_to_whitelist_when_caller_is_owner(contract: StarknetContract): 98 | # When 99 | await contract.forceRegister(address1.public_key).invoke(caller_address=owner.public_key) 100 | 101 | # Then 102 | execution = await contract.isAllowed(address1.public_key).call() 103 | assert execution.result.is_allowed == 1 104 | execution = await contract.freeSlotsCount().call() 105 | assert execution.result.free_slots_count == 9 106 | 107 | @pytest.mark.asyncio 108 | async def it_should_fail_when_caller_is_not_owner(contract: StarknetContract): 109 | # When 110 | with pytest.raises(StarkException): 111 | await contract.forceRegister(address1.public_key).invoke(caller_address=not_owner.public_key) 112 | 113 | # Then 114 | execution = await contract.isAllowed(address1.public_key).call() 115 | assert execution.result.is_allowed == 0 116 | execution = await contract.freeSlotsCount().call() 117 | assert execution.result.free_slots_count == 10 118 | 119 | 120 | def describe_force_register_batch(): 121 | 122 | @pytest.mark.asyncio 123 | async def it_should_add_addresses_to_whitelist_when_caller_is_owner(contract: StarknetContract): 124 | # When 125 | await contract.forceRegisterBatch([address1.public_key, 126 | address2.public_key]).invoke(caller_address=owner.public_key) 127 | 128 | # Then 129 | execution = await contract.isAllowed(address1.public_key).call() 130 | assert execution.result.is_allowed == 1 131 | execution = await contract.isAllowed(address2.public_key).call() 132 | assert execution.result.is_allowed == 1 133 | execution = await contract.freeSlotsCount().call() 134 | assert execution.result.free_slots_count == 8 135 | 136 | @pytest.mark.asyncio 137 | async def it_should_fail_when_caller_is_not_owner(contract: StarknetContract): 138 | # When 139 | with pytest.raises(StarkException): 140 | await contract.forceRegisterBatch([address1.public_key, 141 | address2.public_key]).invoke(caller_address=not_owner.public_key) 142 | 143 | # Then 144 | execution = await contract.isAllowed(address1.public_key).call() 145 | assert execution.result.is_allowed == 0 146 | execution = await contract.isAllowed(address2.public_key).call() 147 | assert execution.result.is_allowed == 0 148 | execution = await contract.freeSlotsCount().call() 149 | assert execution.result.free_slots_count == 10 150 | 151 | 152 | def describe_transfer_ownership(): 153 | 154 | @pytest.mark.asyncio 155 | async def it_should_succeed_when_caller_is_owner(contract: StarknetContract): 156 | # When 157 | await contract.transferOwnership(address1.public_key).invoke(caller_address=owner.public_key) 158 | 159 | # Then 160 | execution = await contract.getOwner().call() 161 | assert execution.result.owner == address1.public_key 162 | 163 | @pytest.mark.asyncio 164 | async def it_should_fail_when_caller_is_not_owner(contract: StarknetContract): 165 | # When 166 | with pytest.raises(StarkException): 167 | await contract.transferOwnership(address1.public_key).invoke(caller_address=not_owner.public_key) 168 | 169 | # Then 170 | execution = await contract.getOwner().call() 171 | assert execution.result.owner == owner.public_key 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------