├── requirements.txt ├── package.json ├── .gitignore ├── .vimspector.json ├── cmd_factory.py ├── LICENSE ├── __init__.py ├── tests ├── test_utils.py ├── conftest.py ├── test_imx_db.py ├── test_imx_objects.py └── test_imx_client.py ├── README.md ├── utils.py ├── imx_client.py ├── imx └── imx.ts ├── imx_db.py ├── tsconfig.json └── imx_objects.py /requirements.txt: -------------------------------------------------------------------------------- 1 | urlpath 2 | easydict 3 | pydantic 4 | typingx 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@imtbl/imx-sdk": "^1.1.7" 4 | }, 5 | "devDependencies": { 6 | "@types/yargs": "^17.0.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .VSCodeCounter 3 | 4 | # python 5 | **/__pycache__/** 6 | .coverage 7 | .hypothesis 8 | 9 | # node 10 | **/node_modules/** 11 | **/build/** 12 | 13 | -------------------------------------------------------------------------------- /.vimspector.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": { 3 | "run": { 4 | "adapter": "debugpy", 5 | "configuration": { 6 | // "name": "debug python", 7 | "type": "python", 8 | "request": "launch", 9 | "stopOnEntry": false, 10 | "console": "integratedTerminal", 11 | 12 | "cwd": "${filedDirname}", 13 | "environment": [], 14 | 15 | "program": "${workspaceFolder}/", 16 | "args": [] 17 | }, 18 | "breakpoints": { 19 | "exception": { 20 | "raised": "N", 21 | "uncaught": "" 22 | } 23 | } 24 | } 25 | // , 26 | //"attach": { 27 | // "adapter": "multi-session", 28 | // "configuration": { 29 | // "request": "attach" 30 | // }, 31 | // "breakpoints": { 32 | // "exception": { 33 | // "raised": "N", 34 | // "uncaught": "" 35 | // } 36 | // } 37 | //} 38 | } 39 | } 40 | // vim:ft=jsonc 41 | -------------------------------------------------------------------------------- /cmd_factory.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | 5 | class CMDFactory: 6 | @staticmethod 7 | def make(base_params, params): 8 | if sys.platform == "linux": 9 | envkw = "export" 10 | separator = ";" 11 | encloser = "'" 12 | elif sys.platform == "win32": 13 | envkw = "set" 14 | separator = "&" 15 | encloser = "" 16 | else: 17 | raise ValueError(f"Unsupported platform: {sys.platform}") 18 | 19 | set_env = ( 20 | lambda varname, var: f"{envkw} {varname}={encloser}{var}{encloser} {separator} " 21 | ) 22 | 23 | working_dir = Path(__file__).parent 24 | cmd = f'cd "{working_dir}" {separator} ' 25 | cmd += set_env("BASE_PARAMS", f"{base_params.json()}") 26 | cmd += set_env("PARAMS", f"{params.json() if params is not None else {}}") 27 | cmd += f"node ./build/imx.js" 28 | 29 | return cmd 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 dimfred.1337@web.de 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to the following 8 | conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies 11 | or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 dimfred.1337@web.de 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to the following 8 | # conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies 11 | # or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | import sys 20 | from pathlib import Path 21 | 22 | # https://imgflip.com/i/5xlpwv 23 | imxpy_path = str(Path(__file__).parent.parent) 24 | if imxpy_path not in sys.path: 25 | sys.path.insert(0, imxpy_path) 26 | 27 | 28 | from imxpy.imx_client import IMXClient 29 | from imxpy.imx_objects import * 30 | from imxpy.utils import paginate, all_pages, linear_retry, SafeNumber, IMXTime 31 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from utils import SafeNumber 3 | 4 | 5 | class TestSafeNumber: 6 | def test_okay_1_eth(self, one_eth): 7 | n = SafeNumber(1, decimals=18, as_wei=False).value 8 | assert len(str(n)) == 19 9 | assert n == str(one_eth) 10 | 11 | n = SafeNumber("1", decimals=18, as_wei=False).value 12 | assert len(str(n)) == 19 13 | assert n == str(one_eth) 14 | 15 | def test_okay_returns_wei(self, one_eth): 16 | n = SafeNumber(one_eth, decimals=18, as_wei=True).value 17 | assert n == str(one_eth) 18 | 19 | def test_okay_leading_zeros_removed(self, one_eth): 20 | n = SafeNumber("0.01", decimals=3, as_wei=False).value 21 | assert n == "10" 22 | 23 | def test_okay_with_number_before_and_after_comma(self): 24 | n = SafeNumber("1.02", decimals=3, as_wei=False).value 25 | assert n == "1020" 26 | 27 | def test_okay_0_5_eth(self, half_eth): 28 | n = SafeNumber("0.5", decimals=18, as_wei=False).value 29 | assert n == str(half_eth) 30 | 31 | def test_okay_0_eth(self): 32 | n = SafeNumber("0", decimals=18, as_wei=False).value 33 | assert n == "0" 34 | 35 | n = SafeNumber(0, decimals=18, as_wei=False).value 36 | assert n == "0" 37 | 38 | def test_fails_with_float(self): 39 | with pytest.raises(ValueError) as e: 40 | SafeNumber(1.0) 41 | 42 | assert "Only 'str' and 'int'" in str(e.value) 43 | 44 | def test_fails_with_comma(self): 45 | with pytest.raises(ValueError) as e: 46 | SafeNumber("1,2", as_wei=True) 47 | assert "invalid literal" in str(e.value) 48 | 49 | def test_fails_too_much_decimals(self): 50 | n = "0.12345" 51 | with pytest.raises(ValueError) as e: 52 | SafeNumber(n, decimals=4) 53 | assert "More decimals" in str(e.value) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imxpy 2 | 3 | ## USE AT YOUR OWN RISK 4 | 5 | THIS LIBRARY IS IN DEVELOPMENT AND CAN CONTAIN BUGS, USE AT YOUR OWN RISK! I WON'T BE RESPONSIBLE IF YOU LOSE YOUR MONEY! 6 | 7 | ## Build & Install 8 | 9 | // install npm 10 | 11 | // install all dependencies 12 | npm install 13 | 14 | // build the typescript file 15 | tsc 16 | 17 | ## Examples 18 | 19 | from imxpy import IMXClient 20 | 21 | client = IMXClient( 22 | net="
, n_workers=, pk="" 23 | ) 24 | 25 | # see imx_objects for parameter types 26 | some_params = SomeParams() 27 | 28 | # the client returns a future 29 | future = client.some_function(some_params) 30 | # resolve the future 31 | res = future.result() 32 | 33 | # if not interested in returned results, make the client shutdown its running processes 34 | client.shutdown() 35 | 36 | Other examples on how to use the `client` correctly can be found in `tests/test_imx_client.py`. Tests starting with `test_okay_*`, are meant to show the correct usage of the library, whereas the others show wrong behavior. 37 | 38 | ## Feature Overview 39 | 40 | Fell free to submit any feature requests / proposals through the issues. 41 | 42 | ### Signable 43 | 44 | - [x] `register` 45 | - [x] `approveNFT` 46 | - [x] `approveERC20` 47 | - [x] `deposit` 48 | - [ ] `depositCancel` 49 | - [ ] `depositReclaim` 50 | - [x] `prepareWithdrawal` 51 | - [x] `completeWithdrawal` 52 | - [x] `transfer` 53 | - [x] `burn` 54 | - [x] `signMessage` (it is there, but IMX currently just returns a success) 55 | - [x] `mint` 56 | - [x] `createOrder` 57 | - [x] `cancelOrder` 58 | - [x] `createTrade` 59 | - [x] `createExchange` 60 | - [x] `createProject` 61 | - [x] `createCollection` 62 | - [x] `updateCollection` 63 | - [x] `addMetadataSchemaToCollection` 64 | - [x] `updateMetadataSchemaByName` 65 | 66 | ### Database 67 | 68 | - [x] `applications` 69 | - [x] `list` 70 | - [x] `details` 71 | - [x] `assets` 72 | - [x] `list` 73 | - [x] `details` 74 | - [x] `balances` 75 | - [x] `list` 76 | - [x] `token balance` 77 | - [ ] `TLV Info` // seems to be version one and replaced by claims 78 | - [x] `collections` 79 | - [x] `list` 80 | - [x] `details` 81 | - [x] `filters` 82 | - [x] `metadataSchema` 83 | - [x] `depostis` 84 | - [x] `list` 85 | - [x] `details` 86 | - [x] `mints` 87 | - [x] `mintable_token` 88 | - [x] `mintable_token_with_addr` 89 | - [x] `mints` 90 | - [x] `mints details` 91 | - [x] `orders 92 | - [x] `list` 93 | - [x] `details` 94 | - [x] `claims 95 | - [x] `starkkeys` 96 | - [x] `transfers` 97 | - [x] `list` 98 | - [x] `details` 99 | - [ ] `withdrawals` 100 | - [ ] `list` 101 | - [ ] `details` 102 | - [ ] `snapshot` 103 | - [x] `tokens` 104 | - [x] `list` 105 | - [x] `details` 106 | - [x] `trades` 107 | - [x] `list` 108 | - [x] `details` 109 | 110 | ## Known Issues 111 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 dimfred.1337@web.de 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to the following 8 | # conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies 11 | # or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | import sys 20 | import datetime as dt 21 | import time 22 | from pydantic import BaseModel, Field, validator 23 | from typing import Union, Optional 24 | 25 | 26 | def ensure_pk(func): 27 | def deco(self, *args, **kwargs): 28 | if self.pk is None: 29 | print("Aborting: Please provide a private_key to call this function.") 30 | sys.exit() 31 | 32 | return func(self, *args, **kwargs) 33 | 34 | return deco 35 | 36 | 37 | def no_retry(f, *args, **kwargs): 38 | return f(*args, **kwargs) 39 | 40 | 41 | def linear_retry(f, *args, post_call=lambda x: x, **kwargs): 42 | for try_ in range(1, 10000): 43 | try: 44 | res = f(*args, **kwargs) 45 | res = post_call(res) 46 | 47 | return res 48 | except: 49 | time.sleep(try_) 50 | 51 | 52 | def paginate(func, *args, retry_strategy=no_retry, **kwargs): 53 | cursor = "" 54 | while True: 55 | res = retry_strategy(func, *args, **kwargs, cursor=cursor) 56 | # DEBUG 57 | # print(res) 58 | cursor = res["cursor"] 59 | if res is None: 60 | break 61 | 62 | res = res["result"] 63 | if not res: 64 | return 65 | 66 | yield res 67 | 68 | if not cursor: 69 | break 70 | 71 | 72 | def all_pages(func, *args, key=None, retry_strategy=no_retry, **kwargs): 73 | results = [] 74 | for res in paginate(func, *args, retry_strategy=retry_strategy, **kwargs): 75 | results.extend(res) 76 | 77 | if key is not None: 78 | results = make_unique(results, key=key) 79 | 80 | return results 81 | 82 | 83 | def make_unique(iterable, key=None): 84 | keys = set() 85 | idxs_to_remove = [] 86 | for idx, val in enumerate(iterable): 87 | k = key(val) 88 | if k not in keys: 89 | keys.add(k) 90 | else: 91 | idxs_to_remove.append(idx) 92 | 93 | for counter, idx in enumerate(idxs_to_remove): 94 | iterable.pop(idx - counter) 95 | 96 | return iterable 97 | 98 | 99 | class IMXTime: 100 | format_ = "%Y-%m-%dT%H:%M:%S.%fZ" 101 | 102 | @staticmethod 103 | def from_str(timestamp_str): 104 | return dt.datetime.strptime(timestamp_str, IMXTime.format_) 105 | 106 | @staticmethod 107 | def to_str(timestamp): 108 | return timestamp.strftime(IMXTime.format_) 109 | 110 | @staticmethod 111 | def now(): 112 | return dt.datetime.utcnow() 113 | 114 | 115 | class SafeNumber: 116 | def __init__(self, number, decimals=None, as_wei=False): 117 | self.value = self.convert_to_safe(number, decimals, as_wei) 118 | 119 | def convert_to_safe(self, number, decimals, as_wei): 120 | if not isinstance(number, (int, str)): 121 | raise ValueError("SafeNumber: Only 'str' and 'int' numbers allowed.") 122 | 123 | if as_wei: 124 | # raises if there are fobidden chars in str 125 | return str(int(number)) 126 | 127 | number = str(number) 128 | if "." not in number: 129 | before_comma, after_comma = number, "" 130 | else: 131 | before_comma, after_comma = number.split(".") 132 | 133 | if len(after_comma) > decimals: 134 | raise ValueError( 135 | f"More decimals present than allowed\n\tnumber: {number}\n\t:decimals: {decimals}" 136 | ) 137 | 138 | len_padding = decimals - len(after_comma) 139 | padding = "".join("0" for _ in range(len_padding)) 140 | 141 | safe_number = "" 142 | if int(before_comma): 143 | safe_number = before_comma + after_comma + padding 144 | # happens when a 0 is put in 145 | elif not after_comma: 146 | safe_number = "0" 147 | else: 148 | # remove leading zeros and append 149 | safe_number = str(int(after_comma)) + padding 150 | 151 | return safe_number 152 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sys 3 | import time 4 | 5 | # add parent dir of imxpy 6 | sys.path.insert(0, str(Path(__file__).parent.parent.parent)) 7 | 8 | 9 | from easydict import EasyDict as edict 10 | import pytest 11 | 12 | from imx_client import IMXClient 13 | from imx_objects import * 14 | 15 | 16 | def random_number(): 17 | import random 18 | 19 | return random.randint(0, 100000000000000000000000000000000000) 20 | 21 | 22 | @pytest.fixture 23 | def random_str(): 24 | return str(random_number()) 25 | 26 | 27 | @pytest.fixture 28 | def acc1(): 29 | acc = edict() 30 | acc.pk = "4c4b2554e43b374f4cafdd5adaeea5e9aff9b3be54d329bc939752bb747294b9" 31 | acc.addr = "0x77406103701907051070fc029e0a90d5be82f76c" 32 | 33 | return acc 34 | 35 | 36 | @pytest.fixture 37 | def acc2(): 38 | acc = edict() 39 | acc.pk = "ac5d52cc7f75e293ecf2a95f3fafef23c9f5345b4a434ed5bacffccbdbe944fd" 40 | acc.addr = "0xea047d1919b732a4b9b12337a60876536f4f2659" 41 | 42 | return acc 43 | 44 | 45 | @pytest.fixture 46 | def acc3(): 47 | acc = edict() 48 | acc.pk = "bfde975ea5aa3779c7e2f2aade7c2a594b53e32ee23a2ae395927ec5fce4aa4b" 49 | acc.addr = "0xd5f5ad7968147c2e198ddbc40868cb1c6f059c6d" 50 | 51 | return acc 52 | 53 | 54 | @pytest.fixture 55 | def one_eth(): 56 | return 1_000_000_000_000_000_000 57 | 58 | 59 | @pytest.fixture 60 | def half_eth(one_eth): 61 | return one_eth // 2 62 | 63 | 64 | @pytest.fixture(scope="function") 65 | def client(acc1): 66 | return IMXClient("test", pk=acc1.pk) 67 | 68 | 69 | @pytest.fixture(scope="function") 70 | def mainnet_client(): 71 | return IMXClient("main") 72 | 73 | 74 | @pytest.fixture(scope="function") 75 | def client2(acc2): 76 | return IMXClient("test", pk=acc2.pk) 77 | 78 | 79 | @pytest.fixture(scope="function") 80 | def project_id(client, acc1): 81 | params = CreateProjectParams( 82 | name="test_proj", company_name="test_company", contact_email="test@test.com" 83 | ) 84 | 85 | res = client.create_project(params) 86 | res = res.result() 87 | 88 | return res["result"]["id"] 89 | 90 | 91 | @pytest.fixture(scope="function") 92 | def random_addr(): 93 | import random 94 | 95 | allowed = "abcdef0123456789" 96 | addr = f"0x{''.join(random.choice(allowed) for _ in range(40))}" 97 | 98 | return addr 99 | 100 | 101 | @pytest.fixture 102 | def contract_addr(): 103 | return "0xb72d1aa092cf5b3b50dabb55bdab0f33dfab37b7" 104 | 105 | 106 | @pytest.fixture 107 | def unregistered_contract_addr(): 108 | return "0xb55016be31047c16c951612f3b0f7c5f92f1faf5" 109 | 110 | 111 | @pytest.fixture(scope="function") 112 | def token_id(client2, acc1, acc2, contract_addr): 113 | _token_id = 0 114 | yield _token_id 115 | 116 | params = TransferParams( 117 | sender=acc2.addr, 118 | receiver=acc1.addr, 119 | token=ERC721(token_id=_token_id, contract_addr=contract_addr), 120 | ) 121 | client2.transfer(params) 122 | 123 | 124 | def mint_params(contract_addr, id_, addr): 125 | params = MintParams( 126 | contract_addr=contract_addr, 127 | targets=[ 128 | MintTarget( 129 | addr=addr, 130 | tokens=[ 131 | MintableToken( 132 | id=id_, 133 | blueprint=str(id_), 134 | ), 135 | ], 136 | ), 137 | ], 138 | ) 139 | 140 | return params 141 | 142 | 143 | @pytest.fixture(scope="function") 144 | def minted_nft_id(client, acc1, contract_addr): 145 | token_id = random_number() 146 | params = mint_params(contract_addr, token_id, acc1.addr) 147 | res = client.mint(params) 148 | res = res.result() 149 | 150 | # wait until the database has applied the state 151 | time.sleep(2) 152 | 153 | return token_id 154 | 155 | 156 | @pytest.fixture(scope="function") 157 | def valid_order_params(client, client2, acc2, contract_addr): 158 | # client1 is in control of the sc therefore he mints to acc2 159 | token_id = random_number() 160 | params = mint_params(contract_addr, token_id, acc2.addr) 161 | res = client.mint(params) 162 | time.sleep(2) 163 | 164 | # client2 now has the nft and can create the order which client1 will buy 165 | params = CreateOrderParams( 166 | sender=acc2.addr, 167 | token_sell=ERC721(token_id=token_id, contract_addr=contract_addr), 168 | token_buy=ETH(quantity="0.000001"), 169 | ) 170 | res = client2.create_order(params) 171 | res = res.result() 172 | 173 | time.sleep(2) 174 | 175 | return (res["result"]["order_id"], token_id) 176 | 177 | 178 | @pytest.fixture 179 | def unregistered_addr(): 180 | return "0xd2Bf8229D98716abEA9D22453C5C5613078B2c46" 181 | 182 | 183 | @pytest.fixture 184 | def erc20_contract_addr(): 185 | return "0x4c04c39fb6d2b356ae8b06c47843576e32a1963e" 186 | 187 | 188 | @pytest.fixture 189 | def gods_unchained_addr(): 190 | return "0xacb3c6a43d15b907e8433077b6d38ae40936fe2c" 191 | 192 | @pytest.fixture 193 | def gods_addr(): 194 | return "0xccc8cb5229b0ac8069c51fd58367fd1e622afd97" 195 | -------------------------------------------------------------------------------- /tests/test_imx_db.py: -------------------------------------------------------------------------------- 1 | class TestRegistration: 2 | def test_okay_is_registered(self, client, acc1): 3 | res = client.db.is_registered(acc1.addr) 4 | assert res 5 | 6 | def test_fail_is_registered(self, client, unregistered_addr): 7 | res = client.db.is_registered(unregistered_addr) 8 | assert not res 9 | 10 | 11 | class TestApplications: 12 | def test_okay_list_applications(self, client): 13 | res = client.db.applications() 14 | 15 | assert res["result"][0]["id"], res 16 | 17 | def test_okay_application_details(self, client): 18 | gog_id = "12f2d631-db48-8891-350c-c74647bb5b7f" 19 | res = client.db.application(gog_id) 20 | 21 | assert res["id"] == gog_id, res 22 | assert res["name"] == "Guilds Of Guardians", res 23 | assert res["created_at"] == "2021-07-02T02:54:02.592523Z", res 24 | 25 | 26 | class TestAssets: 27 | def test_okay_list_assets(self, client): 28 | res = client.db.assets(page_size=10) 29 | 30 | assert res["result"][0]["token_address"], res 31 | 32 | def test_okay_asset_details(self, client): 33 | token_id = "1081884248542" 34 | contract_addr = "0x21a8eba2687c99f5f67093b019bd8d9252b47638" 35 | 36 | res = client.db.asset(token_id, contract_addr) 37 | 38 | assert res["token_address"], res 39 | 40 | 41 | class TestPagination: 42 | def test_okay_pagination_all_transactions_distinct(self, client): 43 | from utils import paginate 44 | 45 | prev = "" 46 | for counter, res in enumerate( 47 | paginate( 48 | client.db.transfers, 49 | min_timestamp="2021-12-12T13:24:52.0Z", 50 | max_timestamp="2021-12-12T13:24:53.987539Z", 51 | page_size=1, 52 | ) 53 | ): 54 | if counter == 3: 55 | break 56 | 57 | cur = res[0]["transaction_id"] 58 | assert cur != prev 59 | prev = cur 60 | 61 | def test_okay_all_pages(self, client, acc1): 62 | from utils import all_pages 63 | 64 | res = all_pages( 65 | client.db.transfers, 66 | sender=acc1.addr, 67 | page_size=1, 68 | min_timestamp="2021-12-12T13:24:52.0Z", 69 | max_timestamp="2021-12-12T13:24:53.987539Z", 70 | ) 71 | 72 | txids = [r["transaction_id"] for r in res] 73 | assert len(txids) == 5, res 74 | # all distinct 75 | assert len(txids) == len(set(txids)), res 76 | 77 | 78 | class TestBalances: 79 | def test_okay_as_wei(self, client, acc1): 80 | res = client.db.balances(acc1.addr) 81 | assert int(res["result"][0]["balance"]) > 800000, res 82 | 83 | def test_okay_token_balance(self, client, acc1, erc20_contract_addr): 84 | res = client.db.balances(acc1.addr, erc20_contract_addr) 85 | 86 | assert res["symbol"] == "GODS", res 87 | assert res["balance"] == "0", res 88 | 89 | 90 | class TestCollections: 91 | def test_okay_list_collections(self, client): 92 | res = client.db.collections() 93 | 94 | assert res["result"][0]["address"], res 95 | 96 | def test_okay_collection_details(self, client, contract_addr): 97 | res = client.db.collection(contract_addr) 98 | 99 | assert res["project_id"] == 864, res 100 | 101 | def test_okay_collection_filters(self, mainnet_client, gods_unchained_addr): 102 | res = mainnet_client.db.collection_filters(gods_unchained_addr) 103 | 104 | assert res, res 105 | 106 | def test_okay_collection_metadata_schema(self, client, contract_addr): 107 | res = client.db.collection_metadata_schema(contract_addr) 108 | 109 | assert any(item["name"] == "test" for item in res), res 110 | 111 | 112 | class TestDeposits: 113 | def test_okay_list_deposits(self, client): 114 | res = client.db.deposits() 115 | 116 | assert res["result"][0]["transaction_id"] 117 | 118 | def test_okay_deposit_details(self, client): 119 | res = client.db.deposit(49905) 120 | 121 | assert res["transaction_id"] == 49905, res 122 | 123 | 124 | class TestOrders: 125 | def test_okay_list_orders(self, client): 126 | res = client.db.orders() 127 | 128 | assert res["result"][0]["order_id"], res 129 | 130 | def test_okay_order_details(self, client): 131 | res = client.db.order(752) 132 | 133 | assert res["order_id"] == 752, res 134 | 135 | 136 | class TestTransfers: 137 | def test_okay_list_transfers(self, client): 138 | res = client.db.transfers() 139 | 140 | assert res["result"][0]["transaction_id"], res 141 | 142 | def test_okay_transfer_details(self, client): 143 | res = client.db.transfer(50314) 144 | 145 | assert res["transaction_id"] == 50314, res 146 | 147 | 148 | # class TestWithdrawals: 149 | # def test_okay_list_withdrawals(self, client): 150 | # res = client.db.withdrawals() 151 | # print(res) 152 | # # assert 153 | 154 | # def test_okay_withdrawal_details(self, client): 155 | # res = client.db.withdrawal() 156 | 157 | # assert res[""] == 158 | 159 | 160 | # TODO not working? 161 | # class TestSnapshot: 162 | # def test_okay_snapshot(self, mainnet_client, gods_unchained_addr): 163 | # res = mainnet_client.db.snapshot(gods_unchained_addr) 164 | 165 | # print(res) 166 | 167 | 168 | class TestTokens: 169 | def test_okay_list_tokens(self, client): 170 | res = client.db.tokens() 171 | 172 | assert res["result"][0]["name"], res 173 | 174 | def test_okay_token_details(self, client): 175 | res = client.db.token("0x4c04c39fb6d2b356ae8b06c47843576e32a1963e") 176 | 177 | assert res["symbol"] == "GODS", res 178 | -------------------------------------------------------------------------------- /imx_client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 dimfred.1337@web.de 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to the following 8 | # conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies 11 | # or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | import json 20 | import subprocess as sp 21 | 22 | from pathlib import Path 23 | from concurrent.futures import ThreadPoolExecutor 24 | 25 | # TODO fix the import stuff, that sucks like that 26 | from imxpy.imx_db import IMXDB 27 | from imxpy.imx_objects import * 28 | from imxpy import utils 29 | from imxpy.cmd_factory import CMDFactory 30 | 31 | 32 | class IMXClient: 33 | def __init__(self, net, n_workers=32, pk=None): 34 | self.pk = pk 35 | self.net = net 36 | self.db = IMXDB(net) 37 | self.pool = ThreadPoolExecutor(n_workers) 38 | 39 | @utils.ensure_pk 40 | def sign_msg(self, params: SignMsgParams, max_retries: int = 1): 41 | return self._run_pool("sign_msg", params, max_retries) 42 | 43 | @utils.ensure_pk 44 | def register(self, max_retries: int = 1): 45 | return self._run_pool("register", None, max_retries) 46 | 47 | @utils.ensure_pk 48 | def create_project(self, params: CreateProjectParams, max_retries: int = 1): 49 | return self._run_pool("create_project", params, max_retries) 50 | 51 | @utils.ensure_pk 52 | def create_collection(self, params: CreateCollectionParams, max_retries: int = 1): 53 | return self._run_pool("create_collection", params, max_retries) 54 | 55 | @utils.ensure_pk 56 | def update_collection(self, params: UpdateCollectionParams, max_retries: int = 1): 57 | return self._run_pool("update_collection", params, max_retries) 58 | 59 | @utils.ensure_pk 60 | def create_metadata_schema( 61 | self, params: CreateMetadataSchemaParams, max_retries: int = 1 62 | ): 63 | return self._run_pool("create_metadata_schema", params, max_retries) 64 | 65 | @utils.ensure_pk 66 | def update_metadata_schema( 67 | self, params: UpdateMetadataSchemaParams, max_retries: int = 1 68 | ): 69 | return self._run_pool("update_metadata_schema", params, max_retries) 70 | 71 | @utils.ensure_pk 72 | def create_exchange(self, params: CreateExchangeParams, max_retries: int = 1): 73 | return self._run_pool("create_exchange", params, max_retries) 74 | 75 | @utils.ensure_pk 76 | def transfer(self, params: TransferParams, max_retries: int = 1): 77 | return self._run_pool("transfer", params, max_retries) 78 | 79 | @utils.ensure_pk 80 | def mint(self, params: MintParams, max_retries: int = 1): 81 | return self._run_pool("mint", params, max_retries) 82 | 83 | @utils.ensure_pk 84 | def burn(self, params: BurnParams, max_retries: int = 1): 85 | return self._run_pool("burn", params, max_retries) 86 | 87 | @utils.ensure_pk 88 | def prepare_withdrawal(self, params: PrepareWithdrawalParams, max_retries: int = 1): 89 | return self._run_pool("prepare_withdrawal", params, max_retries) 90 | 91 | @utils.ensure_pk 92 | def complete_withdrawal( 93 | self, params: CompleteWithdrawalParams, max_retries: int = 1 94 | ): 95 | return self._run_pool("complete_withdrawal", params, max_retries) 96 | 97 | @utils.ensure_pk 98 | def deposit(self, params: DepositParams, max_retries: int = 1): 99 | return self._run_pool("deposit", params, max_retries) 100 | 101 | @utils.ensure_pk 102 | def create_order(self, params: CreateOrderParams, max_retries: int = 1): 103 | return self._run_pool("create_order", params, max_retries) 104 | 105 | @utils.ensure_pk 106 | def cancel_order(self, params: CancelOrderParams, max_retries: int = 1): 107 | return self._run_pool("cancel_order", params, max_retries) 108 | 109 | @utils.ensure_pk 110 | def create_trade(self, params: CreateTradeParams, max_retries: int = 1): 111 | return self._run_pool("create_trade", params, max_retries) 112 | 113 | @utils.ensure_pk 114 | def approve_nft(self, params: ApproveNFTParams, max_retries: int = 1): 115 | return self._run_pool("approve_nft", params, max_retries) 116 | 117 | @utils.ensure_pk 118 | def approve_erc20(self, params: ApproveERC20Params, max_retries: int = 1): 119 | return self._run_pool("approve_erc20", params, max_retries) 120 | 121 | def wait(self): 122 | self.pool.shutdown() 123 | 124 | def _run_pool(self, function_name: str, params=None, max_retries: int = 1): 125 | def _run_cmd(function_name, cmd, max_retries): 126 | for _ in range(max_retries): 127 | res = sp.run(cmd, shell=True, capture_output=True) 128 | res = self._parse_result(res, function_name) 129 | if res["status"] != "error" or not max_retries: 130 | break 131 | 132 | return res 133 | 134 | cmd = self._make_cmd(function_name, params) 135 | return self.pool.submit(_run_cmd, function_name, cmd, max_retries) 136 | 137 | def _make_cmd(self, function_name, params=None): 138 | base_params = BaseParams( 139 | pk=self.pk, network=self.net, function_name=function_name 140 | ) 141 | 142 | cmd = CMDFactory.make(base_params, params) 143 | # DEBUG 144 | # print(cmd) 145 | 146 | return cmd 147 | 148 | def _parse_result(self, res, function_name): 149 | err = res.stderr.decode() 150 | if err: 151 | print(f"[ERROR] {function_name} failed.\n", err) 152 | 153 | res = res.stdout.decode() 154 | # DEBUG whole stdout output 155 | # print(res) 156 | try: 157 | res = json.loads(res) 158 | except Exception as e: 159 | print(f"[ERROR] {function_name}::parse_result failed\n", e) 160 | res = None 161 | 162 | return res 163 | -------------------------------------------------------------------------------- /imx/imx.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | import { AlchemyProvider } from '@ethersproject/providers'; 3 | import { Wallet } from '@ethersproject/wallet'; 4 | import { ImmutableXClient, ETHTokenType, ERC721TokenType, ERC20TokenType } from '@imtbl/imx-sdk' 5 | 6 | const provider = new AlchemyProvider('ropsten', "DvukuyBzEK-JyP6zp1NVeNVYLJCrzjp_"); 7 | 8 | const networkParams = { 9 | test: { 10 | apiUrl: "https://api.uat.x.immutable.com/v1", 11 | starkContractAddr: "0x4527BE8f31E2ebFbEF4fCADDb5a17447B27d2aef", 12 | registratrionContractAddr: "0x6C21EC8DE44AE44D0992ec3e2d9f1aBb6207D864" 13 | }, 14 | main: { 15 | apiUrl: "https://api.x.immutable.com/v1", 16 | starkContractAddr: "0x5FDCCA53617f4d2b9134B29090C87D01058e27e9", 17 | registratrionContractAddr: "0x72a06bf2a1CE5e39cBA06c0CAb824960B587d64c" 18 | } 19 | } 20 | 21 | const CreateIMXClient = async ( 22 | privateKey: string, 23 | network: string, 24 | gasLimit: string = "", 25 | gasPrice: string = ""): Promise => { 26 | let selectedNetwork; 27 | if (network === "test") 28 | selectedNetwork = networkParams.test; 29 | else if (network === "main") 30 | selectedNetwork = networkParams.main; 31 | else 32 | throw Error(`[TYPESCRIPTWRAPPER]: Unknown network type: '${network}'`); 33 | 34 | // TODO do I have to connect each time? 35 | const signer = new Wallet(privateKey).connect(provider); 36 | const client = await ImmutableXClient.build({ 37 | publicApiUrl: selectedNetwork.apiUrl, 38 | signer: signer, 39 | starkContractAddress: selectedNetwork.starkContractAddr, 40 | registrationContractAddress: selectedNetwork.registratrionContractAddr, 41 | gasLimit: gasLimit, 42 | gasPrice: gasPrice, 43 | }); 44 | 45 | return client; 46 | } 47 | 48 | const createSuccessMsg = (msg: any) => { 49 | return { 50 | "status": "success", 51 | "result": msg 52 | } 53 | }; 54 | 55 | const createErrorMsg = (msg: any) => { 56 | return { 57 | "status": "error", 58 | "result": msg 59 | } 60 | }; 61 | 62 | (async (): Promise => { 63 | 64 | const baseParams = JSON.parse(process.env.BASE_PARAMS!); 65 | const params = JSON.parse(process.env.PARAMS!); 66 | 67 | const client = await CreateIMXClient( 68 | baseParams.pk, 69 | baseParams.network, 70 | baseParams.gas_limit, 71 | baseParams.gas_price 72 | ); 73 | 74 | let res, msg; 75 | switch (baseParams.function_name) { 76 | case "sign_msg": { 77 | res = await client.signMessage(params.msg); 78 | msg = createSuccessMsg(res); 79 | break; 80 | } 81 | case "register": { 82 | res = await client.registerImx({ 83 | etherKey: client.address.toLowerCase(), 84 | starkPublicKey: client.starkPublicKey 85 | }); 86 | msg = createSuccessMsg(res); 87 | break; 88 | } 89 | case "create_project": { 90 | res = await client.createProject(params); 91 | msg = createSuccessMsg(res); 92 | break; 93 | } 94 | case "create_collection": { 95 | res = await client.createCollection(params); 96 | msg = createSuccessMsg(res); 97 | break; 98 | } 99 | case "update_collection": { 100 | res = await client.updateCollection( 101 | params.contractAddress, params.params 102 | ); 103 | msg = createSuccessMsg(res); 104 | break; 105 | } 106 | case "create_metadata_schema": { 107 | res = await client.addMetadataSchemaToCollection( 108 | params.contractAddress, params.params 109 | ); 110 | msg = createSuccessMsg(res); 111 | break; 112 | } 113 | case "update_metadata_schema": { 114 | res = await client.updateMetadataSchemaByName( 115 | params.name, params.contractAddress, params.params 116 | ); 117 | msg = createSuccessMsg(res); 118 | break; 119 | } 120 | case "transfer": { 121 | params.quantity = BigNumber.from(params.quantity); 122 | res = await client.transfer(params); 123 | msg = createSuccessMsg(res); 124 | break; 125 | } 126 | case "mint": { 127 | res = await client.mintV2(params); 128 | msg = createSuccessMsg(res.results); 129 | break; 130 | } 131 | case "burn": { 132 | res = await client.burn(params); 133 | msg = createSuccessMsg(res); 134 | break; 135 | } 136 | case "prepare_withdrawal": { 137 | res = await client.prepareWithdrawal(params); 138 | msg = createSuccessMsg(res); 139 | break; 140 | } 141 | case "complete_withdrawal": { 142 | params["starkPublicKey"] = client.starkPublicKey; 143 | res = await client.completeWithdrawal(params); 144 | msg = createSuccessMsg(res); 145 | break; 146 | } 147 | case "deposit": { 148 | res = await client.deposit(params); 149 | msg = createSuccessMsg(res); 150 | break; 151 | } 152 | case "deposit_cancel": { 153 | throw new Error("deposit_cancel not implemented"); 154 | break; 155 | } 156 | case "deposit_reclaim": { 157 | throw new Error("deposit_reclaim not implemented"); 158 | break; 159 | } 160 | case "create_order": { 161 | res = await client.createOrder(params); 162 | msg = createSuccessMsg(res); 163 | break; 164 | } 165 | case "cancel_order": { 166 | res = await client.cancelOrder(params.order_id); 167 | msg = createSuccessMsg(res); 168 | break; 169 | } 170 | case "create_trade": { 171 | res = await client.createTrade(params); 172 | msg = createSuccessMsg(res); 173 | break; 174 | } 175 | case "approve_nft": { 176 | res = await client.approveNFT(params); 177 | msg = createSuccessMsg(res); 178 | break; 179 | } 180 | case "approve_erc20": { 181 | params.amount = BigNumber.from(params.amount); 182 | res = await client.approveERC20(params); 183 | msg = createSuccessMsg(res); 184 | break; 185 | } 186 | case "create_exchange": { 187 | res = await client.createExchange(params.wallet_addr); 188 | msg = createSuccessMsg(res); 189 | break; 190 | } 191 | default: { 192 | throw new Error(`Invalid method name: '${baseParams.method_name}'`); 193 | } 194 | } 195 | 196 | // log result to stdout to be parsed by the python process 197 | console.log(JSON.stringify(msg)); 198 | 199 | })().catch((e) => { 200 | let msgStr = e.message.toString(); 201 | let msg; 202 | try { 203 | msg = JSON.parse(msgStr)["message"]; 204 | } 205 | catch (e_) { 206 | msg = `[TYPESCRIPTWRAPPER]: ${msgStr}`; 207 | } 208 | 209 | let err = createErrorMsg(msg); 210 | console.log(JSON.stringify(err)); 211 | 212 | process.exit(1); 213 | }); 214 | -------------------------------------------------------------------------------- /tests/test_imx_objects.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from pprint import pprint 4 | from pydantic import ValidationError 5 | import itertools as it 6 | 7 | from imx_objects import * 8 | 9 | 10 | class TestCollectionParams: 11 | def test_okay_update_collection(self, acc1): 12 | expected = { 13 | "contractAddress": acc1.addr, 14 | "params": { 15 | "name": "test", 16 | "description": None, 17 | "icon_url": None, 18 | "metadata_api_url": None, 19 | "collection_image_url": None, 20 | }, 21 | } 22 | params = UpdateCollectionParams(contract_addr=acc1.addr, name="test") 23 | 24 | assert params.dict() == expected 25 | 26 | 27 | class TestTransferParams: 28 | def test_okay_eth(self, one_eth, acc1, acc2): 29 | expected = { 30 | "sender": acc1.addr, 31 | "receiver": acc2.addr, 32 | "token": {"type": "ETH", "data": {"decimals": 18}}, 33 | "quantity": str(one_eth), 34 | } 35 | 36 | params = TransferParams( 37 | sender=acc1.addr, receiver=acc2.addr, token=ETH(quantity=1) 38 | ) 39 | assert params.dict() == expected 40 | # it is okay if this test fails since it is dependent on the order of the dict 41 | # it is just here for me to check it at least once, that the same stuff comes out 42 | # of both and json() uses dict() 43 | assert params.json() == json.dumps(expected, separators=(",", ":")) 44 | 45 | def test_okay_erc721(self, acc1, acc2): 46 | expected = { 47 | "sender": acc1.addr, 48 | "receiver": acc2.addr, 49 | "token": { 50 | "type": "ERC721", 51 | "data": {"tokenAddress": acc1.addr, "tokenId": "1"}, 52 | }, 53 | "quantity": "1", 54 | } 55 | 56 | for quantity, token_id in it.product(("1", 1), ("1", 1)): 57 | params = TransferParams( 58 | sender=acc1.addr, 59 | receiver=acc2.addr, 60 | token=ERC721(token_id=token_id, contract_addr=acc1.addr), 61 | ) 62 | assert params.dict() == expected 63 | 64 | # TODO test ERC20 65 | 66 | def test_fails_addr_wrong(self): 67 | with pytest.raises(ValidationError) as e: 68 | does_not_start_with_0x = "0" 69 | TransferParams(sender=does_not_start_with_0x) 70 | assert "has to start with 0x" in str(e.value) 71 | 72 | with pytest.raises(ValidationError) as e: 73 | too_short = "0x000" 74 | TransferParams(sender=too_short) 75 | assert "Len addr incorrect" in str(e.value) 76 | 77 | with pytest.raises(ValidationError) as e: 78 | too_long = "0x00000000000000000000000000000000000000000000000000" 79 | TransferParams(sender=too_long) 80 | assert "Len addr incorrect" in str(e.value) 81 | 82 | def test_fails_erc721_too_high_quantity(self, acc1, acc2): 83 | with pytest.raises(ValidationError) as e: 84 | TransferParams( 85 | sender=acc1.addr, 86 | receiver=acc2.addr, 87 | data=ERC721(quantity=2, token_id=1, contract_addr=acc1.addr), 88 | ) 89 | assert "Quantity must be" in str(e.value) 90 | 91 | 92 | class TestMintParams: 93 | def test_okay_royalty_float(self, acc1): 94 | expected = {"recipient": acc1.addr, "percentage": 1.0} 95 | 96 | r = Royalty(recipient=acc1.addr, percentage=1.0) 97 | assert r.dict() == expected 98 | 99 | r = Royalty(recipient=acc1.addr, percentage=1) 100 | assert r.dict() == expected 101 | 102 | def test_okay_mintable_without_royalties(self, acc1): 103 | expected = { 104 | "id": "1", 105 | "blueprint": "test", 106 | } 107 | 108 | m = MintableToken(id="1", blueprint="test") 109 | assert m.dict() == expected 110 | 111 | def test_okay_mintable_with_royalties(self, acc1): 112 | expected = { 113 | "id": "1", 114 | "blueprint": "test", 115 | "royalties": [{"recipient": acc1.addr, "percentage": 1.0}], 116 | } 117 | 118 | m = MintableToken( 119 | id="1", 120 | blueprint="test", 121 | royalties=[Royalty(recipient=acc1.addr, percentage=1.0)], 122 | ) 123 | assert m.dict() == expected 124 | 125 | def test_okay_minttarget(self, acc1): 126 | expected = { 127 | "etherKey": acc1.addr, 128 | "tokens": [ 129 | { 130 | "id": "1", 131 | "blueprint": "test", 132 | "royalties": [{"recipient": acc1.addr, "percentage": 1.0}], 133 | } 134 | ], 135 | } 136 | 137 | m = MintTarget( 138 | addr=acc1.addr, 139 | tokens=[ 140 | MintableToken( 141 | id="1", 142 | blueprint="test", 143 | royalties=[Royalty(recipient=acc1.addr, percentage=1.0)], 144 | ) 145 | ], 146 | ) 147 | assert m.dict() == expected 148 | 149 | def test_okay_mintparams_without_royalties(self, acc1): 150 | expected = [ 151 | { 152 | "contractAddress": acc1.addr, 153 | "users": [ 154 | { 155 | "etherKey": acc1.addr, 156 | "tokens": [ 157 | { 158 | "id": "1", 159 | "blueprint": "test", 160 | "royalties": [ 161 | {"recipient": acc1.addr, "percentage": 1.0} 162 | ], 163 | } 164 | ], 165 | } 166 | ], 167 | } 168 | ] 169 | 170 | m = MintParams( 171 | contract_addr=acc1.addr, 172 | targets=[ 173 | MintTarget( 174 | addr=acc1.addr, 175 | tokens=[ 176 | MintableToken( 177 | id="1", 178 | blueprint="test", 179 | royalties=[Royalty(recipient=acc1.addr, percentage=1.0)], 180 | ) 181 | ], 182 | ) 183 | ], 184 | ) 185 | assert m.dict() == expected 186 | 187 | def test_okay_mintparams_with_royalties(self, acc1): 188 | expected = [ 189 | { 190 | "contractAddress": acc1.addr, 191 | "users": [ 192 | { 193 | "etherKey": acc1.addr, 194 | "tokens": [ 195 | { 196 | "id": "1", 197 | "blueprint": "test", 198 | "royalties": [ 199 | {"recipient": acc1.addr, "percentage": 1.0} 200 | ], 201 | } 202 | ], 203 | } 204 | ], 205 | "royalties": [{"recipient": acc1.addr, "percentage": 1.0}], 206 | } 207 | ] 208 | 209 | m = MintParams( 210 | contract_addr=acc1.addr, 211 | royalties=[Royalty(recipient=acc1.addr, percentage=1.0)], 212 | targets=[ 213 | MintTarget( 214 | addr=acc1.addr, 215 | tokens=[ 216 | MintableToken( 217 | id="1", 218 | blueprint="test", 219 | royalties=[Royalty(recipient=acc1.addr, percentage=1.0)], 220 | ) 221 | ], 222 | ) 223 | ], 224 | ) 225 | assert m.dict() == expected 226 | -------------------------------------------------------------------------------- /imx_db.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 dimfred.1337@web.de 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to the following 8 | # conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies 11 | # or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | import json 20 | from pydantic.typing import new_type_supertype 21 | import requests as req 22 | import datetime as dt 23 | from urlpath import URL 24 | 25 | from imxpy.utils import IMXTime 26 | from imxpy import utils 27 | 28 | 29 | class IMXDB: 30 | def __init__(self, net): 31 | if net == "test": 32 | self.base = URL("https://api.ropsten.x.immutable.com") 33 | elif net == "main": 34 | self.base = URL("https://api.x.immutable.com") 35 | else: 36 | raise ValueError(f"Unknown net: '{net}") 37 | 38 | self.urlv1 = self.base / "v1" 39 | self.urlv2 = self.base / "v2" 40 | 41 | self.applications_url = self.urlv1 / "applications" 42 | self.orders_url = self.urlv1 / "orders" 43 | self.assets_url = self.urlv1 / "assets" 44 | self.balances_url = self.urlv2 / "balances" 45 | self.collections_url = self.urlv1 / "collections" 46 | self.deposits_url = self.urlv1 / "deposits" 47 | self.mintable_token_url = self.urlv1 / "mintable-token" 48 | self.mints_url = self.urlv1 / "mints" 49 | self.claims_url = self.urlv1 / "rewards" 50 | self.users_url = self.urlv1 / "users" 51 | self.transfers_url = self.urlv1 / "transfers" 52 | self.withdrawals_url = self.urlv1 / "withdrawals" 53 | self.snapshot_url = self.urlv1 / "snapshot" / "balances" 54 | self.tokens_url = self.urlv1 / "tokens" 55 | self.trades_url = self.urlv1 / "trades" 56 | 57 | def application(self, id): 58 | return self._get(self.applications_url / id) 59 | 60 | def applications( 61 | self, *, order_by="name", direction="asc", page_size=100, cursor="" 62 | ): 63 | params = self._make_params(locals()) 64 | return self._get(self.applications_url, params=params) 65 | 66 | def asset(self, token_id, contract_addr, include_fees=True): 67 | params = {"include_fees": True} 68 | 69 | return self._get(self.assets_url / contract_addr / str(token_id), params=params) 70 | 71 | def assets( 72 | self, 73 | *, 74 | user="", 75 | collection="", 76 | name="", 77 | metadata="", 78 | sell_orders=False, 79 | buy_orders=False, 80 | include_fees=True, 81 | updated_min_timestamp="", 82 | updated_max_timestamp="", 83 | status="imx", 84 | order_by="name", 85 | direction="asc", 86 | page_size=100, 87 | cursor="", 88 | ): 89 | params = self._make_params(locals()) 90 | return self._get(self.assets_url, params=params) 91 | 92 | def balances(self, owner, token_addr=""): 93 | url = self.balances_url / owner 94 | if token_addr: 95 | return self._get(url / token_addr) 96 | 97 | return self._get(url) 98 | 99 | def collection(self, contract_addr): 100 | return self._get(self.collections_url / contract_addr) 101 | 102 | def collections( 103 | self, 104 | *, 105 | blacklist="", 106 | order_by="name", 107 | direction="asc", 108 | page_size=100, 109 | cursor="", 110 | ): 111 | params = self._make_params(locals()) 112 | return self._get(self.collections_url, params=params) 113 | 114 | def collection_filters(self, contract_addr, *, page_size=100, next_page_token=""): 115 | params = {"page_size": page_size, "next_page_token": next_page_token} 116 | return self._get( 117 | self.collections_url / contract_addr / "filters", params=params 118 | ) 119 | 120 | def collection_metadata_schema(self, contract_addr): 121 | return self._get(self.collections_url / contract_addr / "metadata-schema") 122 | 123 | def deposit(self, id): 124 | return self._get(self.deposits_url / str(id)) 125 | 126 | def deposits( 127 | self, 128 | *, 129 | user="", 130 | status="", 131 | token_type="", 132 | token_id="", 133 | asset_id="", 134 | token_address="", 135 | token_name="", 136 | min_quantity="", 137 | max_quantity="", 138 | metadata="", 139 | order_by="", 140 | direction="asc", 141 | min_timestamp="", 142 | max_timestamp="", 143 | page_size=100, 144 | cursor="", 145 | ): 146 | params = self._make_params(locals()) 147 | return self._get(self.deposits_url, params=params) 148 | 149 | def mintable_token(self, imx_token_id=None, token_id=None, contract_addr=None): 150 | if imx_token_id is not None: 151 | return self._get(self.urlv1 / "mintable-token" / imx_token_id) 152 | 153 | return self._get(self.mintable_token_url / contract_addr / str(token_id)) 154 | 155 | def mints(self, imx_token_id): 156 | return self._get(self.mints_url / imx_token_id) 157 | 158 | def order(self, order_id): 159 | return self._get(self.urlv1 / "orders" / str(order_id)) 160 | 161 | def orders( 162 | self, 163 | *, 164 | user="", 165 | sell_token_addr="", 166 | sell_token_type="", 167 | sell_token_name="", 168 | sell_token_id="", 169 | sell_asset_id="", 170 | sell_min_quantity="", 171 | sell_max_quantity="", 172 | sell_metadata="", 173 | buy_token_addr="", 174 | buy_token_type="", 175 | buy_token_name="", 176 | buy_token_id="", 177 | buy_asset_id="", 178 | buy_min_quantity="", 179 | buy_max_quantity="", 180 | buy_metadata="", 181 | include_fees=True, 182 | # asc / desc 183 | direction="asc", 184 | updated_min_timestamp="", 185 | updated_max_timestamp="", 186 | min_timestamp="", 187 | max_timestamp="", 188 | order_by="timestamp", 189 | status="active", 190 | page_size=100, 191 | cursor="", 192 | ): 193 | params = self._make_params(locals()) 194 | return self._get(self.orders_url, params=params) 195 | 196 | def claims(self, addr): 197 | return self._get(self.claims_url / addr) 198 | 199 | def stark_key(self, addr): 200 | return self._get(self.users_url / addr) 201 | 202 | def is_registered(self, addr): 203 | res = self.stark_key(addr) 204 | return "accounts" in res 205 | 206 | def transfer(self, id): 207 | return self._get(self.transfers_url / str(id)) 208 | 209 | def transfers( 210 | self, 211 | *, 212 | sender="", 213 | receiver="", 214 | # order_by="timestamp", 215 | direction="asc", 216 | token_type="ETH", 217 | token_id="", 218 | token_addr="", 219 | min_timestamp="", 220 | max_timestamp="", 221 | page_size=100, 222 | cursor="", 223 | ): 224 | params = self._make_params(locals()) 225 | return self._get(self.transfers_url, params=params) 226 | 227 | def withdrawal(self, id): 228 | return self._get(self.withdrawals_url / str(id)) 229 | 230 | # def withdrawals(self, *) 231 | 232 | def snapshot(self, contract_addr, page_size=100, cursor=""): 233 | params = {"page_size": page_size, "cursor": cursor} 234 | return self._get(self.snapshot_url / contract_addr, params=params) 235 | 236 | def token(self, token_addr=""): 237 | return self._get(self.tokens_url / token_addr) 238 | 239 | def tokens(self, addr="", symbols=""): 240 | params = self._make_params(locals()) 241 | return self._get(self.tokens_url, params=params) 242 | 243 | def trades( 244 | self, 245 | *, 246 | party_a_token_type="", 247 | party_a_token_addr="", 248 | party_a_token_id="", 249 | party_b_token_type="", 250 | party_b_token_addr="", 251 | party_b_token_id="", 252 | min_timestamp="", 253 | max_timestamp="", 254 | direction="asc", 255 | order_by="", 256 | page_size=100, 257 | cursor="", 258 | ): 259 | params = self._make_params(locals()) 260 | return self._get(self.trades_url, params=params) 261 | 262 | def _make_params(self, locals_): 263 | del locals_["self"] 264 | for k, v in list(locals_.items()): 265 | if not isinstance(v, bool) and not v: 266 | del locals_[k] 267 | elif k.endswith("addr"): 268 | del locals_[k] 269 | new_k = k.replace("addr", "address") 270 | locals_[new_k] = v 271 | elif k == "sender": 272 | del locals_[k] 273 | new_k = "user" 274 | locals_[new_k] = v 275 | elif k.endswith("timestamp") and isinstance(v, dt.datetime): 276 | locals_[k] = IMXTime.to_str(v) 277 | 278 | return locals_ 279 | 280 | def _get(self, url, params=None): 281 | res = req.get(url, params=params) 282 | res = json.loads(res.text) 283 | 284 | if "message" in res: 285 | res["status"] = "error" 286 | 287 | return res 288 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "esModuleInterop": true, 5 | "strict": true, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "outDir": "./build", 9 | "skipLibCheck": true 10 | 11 | 12 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 13 | 14 | /* Projects */ 15 | // "incremental": true, /* Enable incremental compilation */ 16 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 17 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 18 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 19 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 20 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 21 | 22 | /* Language and Environment */ 23 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 24 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 25 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 26 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 27 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 28 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 29 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 30 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 31 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 32 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 33 | 34 | /* Modules */ 35 | // "rootDir": "./", /* Specify the root folder within your source files. */ 36 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 37 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 38 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 39 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 40 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 41 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 42 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 43 | // "resolveJsonModule": true, /* Enable importing .json files */ 44 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 57 | // "removeComments": true, /* Disable emitting comments. */ 58 | // "noEmit": true, /* Disable emitting files from a compilation. */ 59 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 60 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 61 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 62 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/test_imx_client.py: -------------------------------------------------------------------------------- 1 | from imx_objects import * 2 | from utils import SafeNumber 3 | import time 4 | 5 | 6 | class TestUtility: 7 | def test_okay_sign_msg(self, client): 8 | params = SignMsgParams(msg="{'test':'test'") 9 | res = client.sign_msg(params) 10 | res = res.result() 11 | 12 | # TODO actually thats not okay currently it returns only 13 | # success but not the signed message 14 | # related to IMX not us 15 | assert res["status"] == "success", res 16 | 17 | def test_okay_user_registered(self, client): 18 | res = client.register() 19 | res = res.result() 20 | 21 | assert res["status"] == "success", res 22 | 23 | def test_okay_project_created(self, client): 24 | params = CreateProjectParams( 25 | name="test_proj", company_name="test_company", contact_email="test@test.com" 26 | ) 27 | res = client.create_project(params) 28 | res = res.result() 29 | 30 | assert res["status"] == "success", res 31 | assert res["result"]["id"], res 32 | 33 | def test_okay_collection_created_and_updated(self, client, project_id, random_addr): 34 | return 35 | 36 | # imx now returns an error when the contract_addr does not contain byte code 37 | # therefore one can't use random_addr anymore 38 | params = CreateCollectionParams( 39 | name="test", 40 | contract_addr=random_addr, 41 | owner_public_key="test", 42 | project_id=project_id, 43 | metadata_api_url="https://test.com", 44 | description="test", 45 | icon_url="https://test.com/icon", 46 | collection_image_url="https://test.com/collection_image", 47 | ) 48 | res = client.create_collection(params) 49 | res = res.result() 50 | 51 | assert res["status"] == "success", res 52 | assert res["result"]["address"] == random_addr, res 53 | 54 | params = UpdateCollectionParams(name="test2", contract_addr=random_addr) 55 | res = client.update_collection(params) 56 | res = res.result() 57 | 58 | # TODO somehow imx returns the wrong values, but they have been updated 59 | # normally, just retry and see what happens 60 | res = client.update_collection(params) 61 | res = res.result() 62 | 63 | assert res["status"] == "success", res 64 | assert res["result"]["name"] == "test2", res 65 | 66 | def test_okay_metadata_schema_added_and_updated( 67 | self, client, contract_addr, random_str 68 | ): 69 | schema = [{"name": random_str, "type": "text", "filterable": False}] 70 | 71 | params = CreateMetadataSchemaParams( 72 | contract_addr=contract_addr, metadata=schema 73 | ) 74 | res = client.create_metadata_schema(params) 75 | res = res.result() 76 | 77 | assert res["status"] == "success", res 78 | 79 | params = UpdateMetadataSchemaParams( 80 | contract_addr=contract_addr, name=random_str, new_name=random_str + "i" 81 | ) 82 | res = client.update_metadata_schema(params) 83 | res = res.result() 84 | 85 | assert res["status"] == "success", res 86 | 87 | def test_okay_create_exchange(self, client, acc1): 88 | pass 89 | # params = CreateExchangeParams(wallet_addr=acc1.addr) 90 | # res = client.create_exchange(params) 91 | # res = res.result() 92 | # TODO currently throws error, probably because it is not possible to create 93 | # on mainnet? However, the call is there and should work correctly 94 | 95 | 96 | class TestTransfer: 97 | def get_balance(self, client, addr): 98 | res = client.db.balances(addr) 99 | return int(res["result"][0]["balance"]) 100 | 101 | def test_okay_simple_eth(self, client, acc1, acc2): 102 | params = TransferParams( 103 | sender=acc1.addr, 104 | receiver=acc2.addr, 105 | token=ETH(quantity="0.00001"), 106 | ) 107 | res = client.transfer(params) 108 | res = res.result() 109 | 110 | assert res["status"] == "success", res 111 | assert res["result"]["transfer_id"], res 112 | 113 | def test_okay_simple_erc721(self, client, token_id, acc1, acc2, contract_addr): 114 | params = TransferParams( 115 | sender=acc1.addr, 116 | receiver=acc2.addr, 117 | token=ERC721(token_id=token_id, contract_addr=contract_addr), 118 | ) 119 | res = client.transfer(params) 120 | res = res.result() 121 | 122 | assert res["status"] == "success", res 123 | assert res["result"]["transfer_id"], res 124 | 125 | # TODO 126 | def test_okay_simple_erc20(self, client, acc1, acc2): 127 | pass 128 | 129 | def test_fails_not_enough_balance(self, client, acc1, acc2): 130 | params = TransferParams( 131 | sender=acc1.addr, receiver=acc2.addr, token=ETH(quantity=100000) 132 | ) 133 | res = client.transfer(params, max_retries=1) 134 | res = res.result() 135 | 136 | assert res["status"] == "error", res 137 | assert "insufficient balance" in res["result"], res 138 | 139 | 140 | class TestMint: 141 | def random_token_id(self): 142 | import random 143 | 144 | return random.randint(0, 1000000000000000000000000000000) 145 | 146 | def test_okay_multiple_targets_and_override_global_royalties( 147 | self, client, acc1, acc2, acc3, contract_addr 148 | ): 149 | tid1 = self.random_token_id() 150 | tid2 = self.random_token_id() 151 | tid3 = self.random_token_id() 152 | 153 | tid1 = self.random_token_id() 154 | 155 | params = MintParams( 156 | contract_addr=contract_addr, 157 | royalties=[Royalty(recipient=acc1.addr, percentage=1.0)], 158 | targets=[ 159 | MintTarget( 160 | addr=acc2.addr, 161 | tokens=[ 162 | MintableToken( 163 | id=tid1, 164 | blueprint="1", 165 | # tests override global royalties 166 | royalties=[Royalty(recipient=acc2.addr, percentage=2.0)], 167 | ), 168 | # tests multiple token mints at a time 169 | MintableToken(id=tid2, blueprint="2"), 170 | ], 171 | ), 172 | # tests multiple user targets at a time 173 | MintTarget( 174 | addr=acc3.addr, tokens=[MintableToken(id=tid3, blueprint="3")] 175 | ), 176 | ], 177 | ) 178 | res = client.mint(params, max_retries=1) 179 | res = res.result() 180 | 181 | assert res["status"] == "success", res 182 | 183 | def test_fails_unregistered_contract_addr( 184 | self, client, acc1, unregistered_contract_addr 185 | ): 186 | params = MintParams( 187 | contract_addr=unregistered_contract_addr, 188 | targets=[ 189 | MintTarget( 190 | addr=acc1.addr, 191 | tokens=[ 192 | MintableToken( 193 | id=self.random_token_id(), 194 | blueprint="1", 195 | ), 196 | ], 197 | ), 198 | ], 199 | ) 200 | res = client.mint(params, max_retries=1) 201 | res = res.result() 202 | 203 | assert res["status"] == "error", res 204 | assert "Unique project error: could not find collections project" in res["result"], res 205 | 206 | def test_fails_duplicate_asset(self, client, contract_addr, acc1): 207 | params = MintParams( 208 | contract_addr=contract_addr, 209 | targets=[ 210 | MintTarget( 211 | addr=acc1.addr, 212 | tokens=[ 213 | MintableToken( 214 | id=0, 215 | blueprint="0", 216 | ) 217 | ], 218 | ) 219 | ], 220 | ) 221 | res = client.mint(params, max_retries=1) 222 | res = res.result() 223 | 224 | assert res["status"] == "error", res 225 | assert "asset, duplicate id" in res["result"], res 226 | 227 | 228 | class TestBurn: 229 | def test_okay_burn(self, client, acc1, contract_addr, minted_nft_id): 230 | # sends the nft to the burn addr, which is 231 | params = BurnParams( 232 | sender=acc1.addr, 233 | token=ERC721(token_id=minted_nft_id, contract_addr=contract_addr), 234 | ) 235 | res = client.burn(params) 236 | res = res.result() 237 | 238 | assert res["status"] == "success", res 239 | assert res["result"]["transfer_id"], res 240 | 241 | 242 | class TestWithdrawal: 243 | def test_okay_prepare(self, client, acc1): 244 | params = PrepareWithdrawalParams( 245 | sender=acc1.addr, token=ETH(quantity="0.0000001") 246 | ) 247 | res = client.prepare_withdrawal(params) 248 | res = res.result() 249 | 250 | assert res["status"] == "success", res 251 | assert res["result"]["withdrawal_id"], res 252 | 253 | def test_okay_complete_withdrawal(self, client, acc1): 254 | # this test is a bit weird, since it can only run if we have 255 | # run prepare_withdrawal before that 256 | 257 | balance = client.db.balances(acc1.addr) 258 | withdrawable = int(balance["result"][0]["withdrawable"]) 259 | if not withdrawable: 260 | msg = "[WARNING] 'test_okay_complete_withdrawal', can't run since there is " 261 | msg += "no asset to withdraw." 262 | print(msg) 263 | return 264 | 265 | params = CompleteWithdrawalParams(token=ETH()) 266 | res = client.complete_withdrawal(params) 267 | res = res.result() 268 | 269 | # always returns success so no help here 270 | assert res["status"] == "success", res 271 | # TODO the result with each withdrawal a new "random" address dunno why yet tho. 272 | # assert res["result"] == acc1.addr 273 | 274 | 275 | class TestDeposit: 276 | def test_okay_deposit(self): 277 | pass 278 | 279 | def test_okay_depostit_cancel(self): 280 | pass 281 | 282 | def test_okay_deposit_reclaim(self): 283 | pass 284 | 285 | 286 | class TestTrading: 287 | def test_okay_order_sell_and_cancel( 288 | self, client, acc1, minted_nft_id, contract_addr 289 | ): 290 | params = CreateOrderParams( 291 | sender=acc1.addr, 292 | token_sell=ERC721(token_id=minted_nft_id, contract_addr=contract_addr), 293 | token_buy=ETH(quantity="0.000001"), 294 | ) 295 | res = client.create_order(params) 296 | res = res.result() 297 | 298 | assert res["status"] == "success", res 299 | assert res["result"]["order_id"], res 300 | 301 | order_id = res["result"]["order_id"] 302 | params = CancelOrderParams(order_id=order_id) 303 | res = client.cancel_order(params) 304 | res = res.result() 305 | 306 | assert res["status"] == "success", res 307 | assert res["result"]["order_id"] == int(order_id), res 308 | assert not res["result"]["status"], res 309 | 310 | def test_okay_order_buy(self): 311 | # TODO I think this didn't work for serveral people, just let it here as 312 | # a reminder to test at some point 313 | pass 314 | 315 | def test_okay_create_trade(self, client, acc1, valid_order_params, contract_addr): 316 | order_id, token_id = valid_order_params 317 | 318 | params = CreateTradeParams( 319 | sender=acc1.addr, 320 | order_id=order_id, 321 | token_buy=ERC721(token_id=token_id, contract_addr=contract_addr), 322 | token_sell=ETH(quantity="0.000001"), 323 | ) 324 | res = client.create_trade(params) 325 | res = res.result() 326 | 327 | assert res["status"] == "success", res 328 | assert res["result"]["trade_id"], res 329 | 330 | 331 | class TestApprovals: 332 | def test_okay_nft(self, client, minted_nft_id, contract_addr): 333 | try: 334 | params = ApproveNFTParams( 335 | token_id=minted_nft_id, contract_addr=contract_addr 336 | ) 337 | res = client.approve_nft(params) 338 | res = res.result() 339 | except: 340 | assert False, f"Failed to approve NFT: {res}" 341 | 342 | def test_okay_erc20(self, client): 343 | params = ApproveERC20Params( 344 | token=ERC20( 345 | quantity="0.01", 346 | contract_addr="0xccc8cb5229b0ac8069c51fd58367fd1e622afd97", 347 | decimals=18, 348 | as_wei=False, 349 | ) 350 | ) 351 | res = client.approve_erc20(params) 352 | res = res.result() 353 | 354 | assert res["status"] == "success", res 355 | assert res["result"], res 356 | -------------------------------------------------------------------------------- /imx_objects.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 dimfred.1337@web.de 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to the following 8 | # conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies 11 | # or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | import json 20 | 21 | from types import new_class 22 | from typing import Optional, Union, Generator, Callable, Any, List, cast 23 | from typingx import isinstancex 24 | 25 | from enum import Enum 26 | 27 | from pydantic import BaseModel, Field, validator 28 | from pydantic.generics import Generic, TypeVar 29 | 30 | from imxpy import utils 31 | 32 | 33 | ######################################################################################## 34 | # UTILS 35 | ######################################################################################## 36 | class Utils: 37 | @staticmethod 38 | def exclude(param, kwargs): 39 | if "exclude" not in kwargs or not kwargs["exclude"]: 40 | kwargs["exclude"] = set() 41 | 42 | if isinstance(param, (list, tuple, set)): 43 | kwargs["exclude"].update(param) 44 | else: 45 | kwargs["exclude"].add(param) 46 | 47 | return kwargs 48 | 49 | @staticmethod 50 | def pop_if_none(kwargs, *keys): 51 | for key in keys: 52 | if kwargs[key] is None: 53 | del kwargs[key] 54 | 55 | 56 | T = TypeVar("T") 57 | 58 | 59 | def _display_type(v: Any) -> str: 60 | try: 61 | return v.__name__ 62 | except AttributeError: 63 | # happens with typing objects 64 | return str(v).replace("typing.", "") 65 | 66 | 67 | # taken from https://github.com/samuelcolvin/pydantic/issues/2079 68 | # that is some sick shit! 69 | class Strict(Generic[T]): 70 | __typelike__: T 71 | 72 | @classmethod 73 | def __class_getitem__(cls, typelike: T) -> T: 74 | new_cls = new_class( 75 | f"Strict[{_display_type(typelike)}]", 76 | (cls,), 77 | {}, 78 | lambda ns: ns.update({"__typelike__": typelike}), 79 | ) 80 | return cast(T, new_cls) 81 | 82 | @classmethod 83 | def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]: 84 | yield cls.validate 85 | 86 | @classmethod 87 | def validate(cls, value: Any) -> T: 88 | if not isinstancex(value, cls.__typelike__): 89 | raise TypeError(f"{value!r} is not of valid type") 90 | return value 91 | 92 | 93 | ######################################################################################## 94 | # VALIDATION 95 | ######################################################################################## 96 | class Validator: 97 | @staticmethod 98 | def validate_addr(addr): 99 | if not addr.startswith("0x"): 100 | raise ValueError(f"Addr has to start with 0x got: {addr}") 101 | 102 | l = len(addr) 103 | if l != 42: 104 | raise ValueError(f"Len addr incorrect: {l} / 42") 105 | 106 | return addr.lower() 107 | 108 | @staticmethod 109 | def validate_token(token): 110 | if token.type in ("ETH", "ERC20"): 111 | safe_number = utils.SafeNumber( 112 | number=token.quantity, decimals=token.decimals, as_wei=token.as_wei 113 | ) 114 | else: # ERC721 115 | safe_number = utils.SafeNumber(number=token.quantity, as_wei=True) 116 | 117 | token.quantity = safe_number.value 118 | return token 119 | 120 | 121 | ######################################################################################## 122 | # BASE 123 | ######################################################################################## 124 | class Base(BaseModel): 125 | def json(self): 126 | return json.dumps(self.dict(), separators=(",", ":")) 127 | 128 | 129 | ######################################################################################## 130 | # SIGN MESSAGE 131 | ######################################################################################## 132 | class SignMsgParams(BaseModel): 133 | msg: str 134 | 135 | 136 | ######################################################################################## 137 | # TOKENS 138 | ######################################################################################## 139 | class TokenType(Enum): 140 | ETH = 0 141 | ERC20 = 1 142 | ERC721 = 2 143 | MINTABLE_ERC721 = 3 144 | 145 | def __str__(self): 146 | return self.name 147 | 148 | 149 | class ETH(Base): 150 | quantity: Union[str, int] = 0 151 | type: str = str(TokenType.ETH) 152 | decimals: int = 18 153 | as_wei: bool = False 154 | 155 | @validator("quantity") 156 | def check_empty(cls, quantity): 157 | return quantity if quantity else 0 158 | 159 | def dict(self, *args, **kwargs): 160 | Utils.exclude("as_wei", kwargs) 161 | d = super().dict(*args, **kwargs) 162 | new_d = {"type": d.pop("type"), "data": d} 163 | 164 | return new_d 165 | 166 | 167 | class ERC20(Base): 168 | symbol: str = "" 169 | tokenAddress: str = Field(alias="contract_addr") 170 | decimals: int = 18 171 | quantity: Union[str, int] = 0 172 | type: str = str(TokenType.ERC20) 173 | as_wei: bool = False 174 | 175 | @validator("tokenAddress") 176 | def validate_addr(cls, addr): 177 | return Validator.validate_addr(addr) 178 | 179 | def dict(self, *args, **kwargs): 180 | Utils.exclude("as_wei", kwargs) 181 | d = super().dict(*args, **kwargs) 182 | new_d = {"type": d.pop("type"), "data": d} 183 | 184 | return new_d 185 | 186 | 187 | class ERC721(Base): 188 | tokenAddress: str = Field(alias="contract_addr") 189 | tokenId: Union[str, int] = Field(alias="token_id") 190 | quantity: Union[str, int] = 1 191 | type: str = str(TokenType.ERC721) 192 | 193 | @validator("quantity") 194 | def ensure_one(cls, quantity): 195 | if quantity != 1: 196 | raise ValueError("Quantity must be '1'.") 197 | 198 | return quantity 199 | 200 | @validator("tokenAddress") 201 | def validate_addr(cls, addr): 202 | return Validator.validate_addr(addr) 203 | 204 | @validator("tokenId") 205 | def to_str(cls, token_id): 206 | return str(token_id) 207 | 208 | def dict(self, *args, **kwargs): 209 | d = super().dict(*args, **kwargs) 210 | new_d = {"type": d.pop("type"), "data": d} 211 | 212 | return new_d 213 | 214 | 215 | ######################################################################################## 216 | # BASE PARAMS 217 | ######################################################################################## 218 | class BaseParams(Base): 219 | pk: str 220 | network: str 221 | function_name: str 222 | # TODO bother later with types and bla 223 | gas_limit: Optional[str] 224 | gas_price: Optional[str] 225 | 226 | 227 | ######################################################################################## 228 | # REGISTRATION & PROJECT & COLLECTION & EXCHANGE 229 | ######################################################################################## 230 | class CreateProjectParams(Base): 231 | name: str 232 | company_name: str 233 | contact_email: str 234 | 235 | 236 | class CreateCollectionParams(Base): 237 | name: str 238 | contract_address: str = Field(alias="contract_addr") 239 | owner_public_key: str 240 | project_id: int 241 | metadata_api_url: Optional[str] 242 | description: Optional[str] 243 | icon_url: Optional[str] 244 | collection_image_url: Optional[str] 245 | 246 | @validator("contract_address") 247 | def validate_addr(cls, addr): 248 | return Validator.validate_addr(addr) 249 | 250 | 251 | class UpdateCollectionParams(Base): 252 | contractAddress: str = Field(alias="contract_addr") 253 | name: Optional[str] 254 | description: Optional[str] 255 | icon_url: Optional[str] 256 | metadata_api_url: Optional[str] 257 | collection_image_url: Optional[str] 258 | 259 | @validator("contractAddress") 260 | def validate_addr(cls, addr): 261 | return Validator.validate_addr(addr) 262 | 263 | def dict(self, *args, **kwargs): 264 | new_d = {} 265 | d = super().dict(*args, **kwargs) 266 | 267 | new_d["contractAddress"] = d.pop("contractAddress") 268 | new_d["params"] = d 269 | 270 | return new_d 271 | 272 | 273 | class CreateMetadataSchemaParams(Base): 274 | contractAddress: str = Field(alias="contract_addr") 275 | # TODO could also define the whole schema as a model 276 | metadata: List[dict] 277 | 278 | def dict(self, *args, **kwargs): 279 | d = super().dict(*args, **kwargs) 280 | d["params"] = {"metadata": d.pop("metadata")} 281 | return d 282 | 283 | 284 | class UpdateMetadataSchemaParams(Base): 285 | name: str 286 | contractAddress: str = Field(alias="contract_addr") 287 | new_name: Optional[str] 288 | new_type: Optional[str] 289 | new_filterable: Optional[bool] 290 | 291 | @validator("contractAddress") 292 | def validate_addr(cls, addr): 293 | return Validator.validate_addr(addr) 294 | 295 | def dict(self): 296 | d = super().dict() 297 | d["params"] = {} 298 | for key in ("name", "type", "filterable"): 299 | new_key = f"new_{key}" 300 | if d[new_key] is None: 301 | del d[new_key] 302 | else: 303 | d["params"][key] = d.pop(new_key) 304 | 305 | return d 306 | 307 | 308 | class CreateExchangeParams(Base): 309 | walletAddress: str = Field(alias="wallet_addr") 310 | 311 | @validator("walletAddress") 312 | def validate_addr(cls, addr): 313 | Validator.validate_addr(addr) 314 | 315 | 316 | ######################################################################################## 317 | # TRANSFER 318 | ######################################################################################## 319 | class TransferParams(Base): 320 | sender: str 321 | receiver: str 322 | token: Strict[Union[ETH, ERC721, ERC20]] 323 | 324 | @validator("sender", "receiver") 325 | def validate_addr(cls, addr): 326 | return Validator.validate_addr(addr) 327 | 328 | @validator("token") 329 | def validate_token(cls, token): 330 | return Validator.validate_token(token) 331 | 332 | def dict(self, *args, **kwargs): 333 | d = super().dict(*args, **kwargs) 334 | d["quantity"] = d["token"]["data"].pop("quantity") 335 | 336 | return d 337 | 338 | 339 | ######################################################################################## 340 | # MINT 341 | ######################################################################################## 342 | class Royalty(Base): 343 | recipient: str 344 | percentage: Union[float, int] 345 | 346 | @validator("recipient") 347 | def validate_addr(cls, addr): 348 | return Validator.validate_addr(addr) 349 | 350 | 351 | class MintableToken(Base): 352 | id: str 353 | blueprint: str 354 | # local royalties for this token, overrides global royalty config 355 | royalties: Optional[List[Royalty]] 356 | 357 | def dict(self, *args, **kwargs): 358 | if self.royalties is None: 359 | Utils.exclude("royalties", kwargs) 360 | 361 | return super().dict(*args, **kwargs) 362 | 363 | 364 | class MintTarget(Base): 365 | etherKey: str = Field(alias="addr") 366 | tokens: List[MintableToken] 367 | 368 | @validator("etherKey") 369 | def validate_addr(cls, addr): 370 | return Validator.validate_addr(addr) 371 | 372 | 373 | class MintParams(Base): 374 | contractAddress: str = Field(alias="contract_addr") 375 | users: List[MintTarget] = Field(alias="targets") 376 | # global royalty config, will get overridden by MintableToken royalties 377 | royalties: Optional[List[Royalty]] 378 | 379 | def dict(self, *args, **kwargs): 380 | if self.royalties is None: 381 | Utils.exclude("royalties", kwargs) 382 | 383 | return [super().dict(*args, **kwargs)] 384 | 385 | 386 | ######################################################################################## 387 | # BURN 388 | ######################################################################################## 389 | class BurnParams(Base): 390 | sender: str 391 | token: Strict[Union[ETH, ERC20, ERC721]] 392 | 393 | @validator("sender") 394 | def validate_addr(cls, addr): 395 | return Validator.validate_addr(addr) 396 | 397 | @validator("token") 398 | def validate_token(cls, token): 399 | return Validator.validate_token(token) 400 | 401 | def dict(self, *args, **kwargs): 402 | d = super().dict(*args, **kwargs) 403 | d["quantity"] = d["token"]["data"].pop("quantity") 404 | 405 | return d 406 | 407 | 408 | ######################################################################################## 409 | # WITHDRAW & DEPOSIT 410 | ######################################################################################## 411 | class PrepareWithdrawalParams(Base): 412 | user: str = Field(alias="sender") 413 | token: Strict[Union[ETH, ERC20, ERC721]] 414 | 415 | @validator("user") 416 | def validate_addr(cls, addr): 417 | return Validator.validate_addr(addr) 418 | 419 | @validator("token") 420 | def validate_token(cls, token): 421 | return Validator.validate_token(token) 422 | 423 | def dict(self, *args, **kwargs): 424 | d = super().dict(*args, **kwargs) 425 | d["quantity"] = d["token"]["data"].pop("quantity") 426 | 427 | return d 428 | 429 | 430 | class CompleteWithdrawalParams(Base): 431 | token: Strict[Union[ETH, ERC20, ERC721]] 432 | 433 | @validator("token") 434 | def validate_token(cls, token): 435 | return Validator.validate_token(token) 436 | 437 | def dict(self, *args, **kwargs): 438 | d = super().dict(*args, **kwargs) 439 | d["token"]["data"].pop("quantity") 440 | 441 | return d 442 | 443 | 444 | class DepositParams(Base): 445 | user: str = Field(alias="sender") 446 | token: Strict[Union[ETH, ERC20, ERC721]] 447 | 448 | @validator("token") 449 | def validate_token(cls, token): 450 | return Validator.validate_token(token) 451 | 452 | def dict(self, *args, **kwargs): 453 | d = super().dict(*args, **kwargs) 454 | d["quantity"] = d["token"]["data"].pop("quantity") 455 | 456 | return d 457 | 458 | 459 | class DepositCancelParams(Base): 460 | # TODO not sure about the getVaults call to obtain a vault id 461 | pass 462 | 463 | 464 | class DepositReclaimParams(Base): 465 | # TODO 466 | pass 467 | 468 | 469 | ######################################################################################## 470 | # TRADING 471 | ######################################################################################## 472 | class CreateOrderParams(Base): 473 | user: str = Field(alias="sender") 474 | tokenSell: Strict[Union[ETH, ERC20, ERC721]] = Field(alias="token_sell") 475 | tokenBuy: Strict[Union[ETH, ERC20, ERC721]] = Field(alias="token_buy") 476 | 477 | @validator("user") 478 | def validate_addr(cls, addr): 479 | return Validator.validate_addr(addr) 480 | 481 | @validator("tokenSell", "tokenBuy") 482 | def validate_token(cls, token): 483 | return Validator.validate_token(token) 484 | 485 | def dict(self, *args, **kwargs): 486 | d = super().dict(*args, **kwargs) 487 | d["amountSell"] = d["tokenSell"]["data"].pop("quantity") 488 | d["amountBuy"] = d["tokenBuy"]["data"].pop("quantity") 489 | 490 | return d 491 | 492 | 493 | class CancelOrderParams(Base): 494 | order_id: Union[str, int] 495 | 496 | @validator("order_id") 497 | def to_int(cls, sn): 498 | return int(sn) 499 | 500 | 501 | class CreateTradeParams(Base): 502 | orderId: int = Field(alias="order_id") 503 | user: str = Field(alias="sender") 504 | tokenSell: Strict[Union[ETH, ERC20, ERC721]] = Field(alias="token_sell") 505 | tokenBuy: Strict[Union[ETH, ERC20, ERC721]] = Field(alias="token_buy") 506 | 507 | @validator("user") 508 | def validate_addr(cls, addr): 509 | return Validator.validate_addr(addr) 510 | 511 | @validator("tokenSell", "tokenBuy") 512 | def validate_token(cls, token): 513 | return Validator.validate_token(token) 514 | 515 | def dict(self, *args, **kwargs): 516 | d = super().dict(*args, **kwargs) 517 | d["amountSell"] = d["tokenSell"]["data"].pop("quantity") 518 | d["amountBuy"] = d["tokenBuy"]["data"].pop("quantity") 519 | 520 | return d 521 | 522 | 523 | ######################################################################################## 524 | # APPROVING 525 | ######################################################################################## 526 | class ApproveNFTParams(Base): 527 | tokenAddress: str = Field(alias="contract_addr") 528 | tokenId: Union[str, int] = Field(alias="token_id") 529 | 530 | @validator("tokenAddress") 531 | def validate_addr(cls, addr): 532 | return Validator.validate_addr(addr) 533 | 534 | @validator("tokenId") 535 | def to_str(cls, token_id): 536 | return str(token_id) 537 | 538 | 539 | class ApproveERC20Params(Base): 540 | token: ERC20 541 | 542 | @validator("token") 543 | def validate_token(cls, token): 544 | return Validator.validate_token(token) 545 | 546 | def dict(self): 547 | params = self.token.dict() 548 | params = params["data"] 549 | del params["symbol"] 550 | del params["decimals"] 551 | amount = params.pop("quantity") 552 | params["amount"] = amount 553 | 554 | return params 555 | --------------------------------------------------------------------------------