├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── UN_logo_on_white.svg ├── mintersdk ├── __init__.py ├── minterapi.py ├── sdk │ ├── __init__.py │ ├── check.py │ ├── deeplink.py │ ├── transactions.py │ └── wallet.py ├── shortcuts.py └── test │ ├── __init__.py │ ├── test_check.py │ ├── test_deeplink.py │ ├── test_transactions.py │ └── test_wallet.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # static files generated from Django application using `collectstatic` 142 | media 143 | static 144 | 145 | # Custom 146 | .idea/ 147 | .DS_Store/ 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 U-node.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft mintersdk 2 | global-exclude __pycache__ 3 | global-exclude *.py[co] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | Python SDK 7 | 8 | Ported from official Minter's php SDK 9 | 10 | Created by https://www.u-node.net's masternode co-founder Roman Matusevich 11 | 12 | You can support our project by sending any Minter's coins to our wallet Mx6e0cd64694b1e143adaa7d4914080e748837aec9 13 | 14 | Feel free to delegate to our 3% masternode Mp02bc3c3f77d5ab9732ef9fc3801a6d72dc18f88328c14dc735648abfe551f50f 15 | 16 | 17 | # Installation 18 | `pip install minter-sdk` 19 | 20 | 21 | 22 | # Using API 23 | ```python 24 | from mintersdk.minterapi import MinterAPI 25 | 26 | node_url = 'https://minter-node-1.testnet.minter.network:8841' # Example of a node url 27 | api = MinterAPI(api_url=node_url) 28 | 29 | # 'connect_timeout', 'read_timeout', 'headers' kwargs would be passed to request, if provided 30 | api = MinterAPI(api_url=node_url, connect_timeout=1, read_timeout=3, headers={}) 31 | ``` 32 | Numeric strings automatically are converted to integers in `response['result']` dict. 33 | 34 | Some API methods accept `pip2bip (bool)` argument to convert coin values from PIP to BIP. 35 | Values are `Decimal` type after conversion. 36 | 37 | ## Methods 38 | - `get_addresses(addresses, height=None, pip2bip=False)` 39 | Returns addresses balances. 40 | 41 | - `get_balance(address, height=None, pip2bip=False)` 42 | Returns coins list, balance and transaction count (for nonce) of an address. 43 | 44 | - `get_block(height, pip2bip=False)` 45 | Returns block data at given height. 46 | 47 | - `get_candidate(public_key, height=None, pip2bip=False)` 48 | Returns candidate’s info by provided public_key. It will respond with 404 code if candidate is not found. 49 | 50 | - `get_candidates(height=None, include_stakes=False, pip2bip=False)` 51 | Returns list of candidates. 52 | 53 | - `get_coin_info(symbol, height=None, pip2bip=False)` 54 | Returns information about coin. Note: this method does not return information about base coins (MNT and BIP). 55 | 56 | - `get_events(height, pip2bip=False)` 57 | Returns events at given height. 58 | 59 | - `get_genesis(pip2bip=False)` 60 | Return network genesis. 61 | 62 | - `get_latest_block_height()` 63 | Returns latest block height. 64 | 65 | - `get_max_gas_price(height=None)` 66 | Returns current max gas price. 67 | 68 | - `get_min_gas_price()` 69 | Returns current min gas price. 70 | 71 | - `get_missed_blocks(public_key, height=None)` 72 | Returns missed blocks by validator public key. 73 | 74 | - `get_network_info()` 75 | Return node network information. 76 | 77 | - `get_nonce(address)` 78 | Returns next transaction number (nonce) of an address. 79 | 80 | - `get_status()` 81 | Returns node status info. 82 | 83 | - `get_transaction(tx_hash, pip2bip=False, decode_payload=False)` 84 | Returns transaction info. 85 | 86 | - `get_transactions(query, page=None, limit=None, pip2bip=False, decode_payload=False)` 87 | Return transactions by query. 88 | 89 | - `get_unconfirmed_transactions(limit=None)` 90 | Returns unconfirmed transactions. 91 | 92 | - `get_validators(height=None, page=None, limit=None)` 93 | Returns list of active validators. 94 | 95 | - `estimate_coin_buy(coin_to_sell, value_to_buy, coin_to_buy, height=None, pip2bip=False)` 96 | Return estimate of buy coin transaction. 97 | Provide value in PIP if `pip2bip is False` or in BIP if `pip2bip is True` 98 | 99 | - `estimate_coin_sell(coin_to_sell, value_to_sell, coin_to_buy, height=None, pip2bip=False)` 100 | Return estimate of sell coin transaction. 101 | Provide value in PIP if `pip2bip is False` or in BIP if `pip2bip is True` 102 | 103 | - `estimate_coin_sell_all(coin_to_sell, value_to_sell, coin_to_buy, height=None, pip2bip=False)` 104 | Return estimate of sell all coin transaction. 105 | Provide value in PIP if `pip2bip is False` or in BIP if `pip2bip is True` 106 | 107 | - `estimate_tx_commission(tx, height=None, pip2bip=False)` 108 | Return estimate of transaction. 109 | 110 | - `send_transaction(tx)` 111 | Returns the result of sending signed tx. 112 | 113 | 114 | 115 | # SDK use 116 | ## Create transactions 117 | Each Minter transaction requires `nonce, gas_coin` to be passed. Also you can pass `payload, chain_id, gas_price`. 118 | 119 | `MiterTx(nonce, gas_coin, payload='', service_data='', chain_id=1, gas_price=1, **kwargs)` 120 | 121 | **To create Minter transaction you MUST use concrete transaction class.** 122 | 123 | All transaction values should be passed in BIP, you shouldn't convert them to PIP. 124 | All coin symbols are case insensitive, e.g. you can pass `gas_coin='BIP'` or `gas_coin='bip'` 125 | 126 | ### Transactions 127 | - MinterBuyCoinTx 128 | ```python 129 | from mintersdk.sdk.transactions import MinterBuyCoinTx 130 | tx = MinterBuyCoinTx(coin_to_buy='SYMBOL', value_to_buy=1, coin_to_sell='SYMBOL', max_value_to_sell=2, nonce=1, gas_coin='SYMBOL') 131 | ``` 132 | 133 | - MinterCreateCoinTx 134 | ```python 135 | from mintersdk.sdk.transactions import MinterCreateCoinTx 136 | tx = MinterCreateCoinTx(name='Coin description', symbol='SYMBOL', initial_amount=1.5, initial_reserve=10000, crr=50, max_supply=1000, nonce=1, gas_coin='SYMBOL') 137 | ``` 138 | 139 | - MinterDeclareCandidacyTx 140 | ```python 141 | from mintersdk.sdk.transactions import MinterDeclareCandidacyTx 142 | tx = MinterDeclareCandidacyTx(address='Mx...', pub_key='Mp...', commission=1, coin='SYMBOL', stake=100, nonce=1, gas_coin='SYMBOL') 143 | ``` 144 | 145 | - MinterDelegateTx 146 | ```python 147 | from mintersdk.sdk.transactions import MinterDelegateTx 148 | tx = MinterDelegateTx(pub_key='Mp...', coin='SYMBOL', stake=100, nonce=1, gas_coin='SYMBOL') 149 | ``` 150 | 151 | - MinterRedeemCheckTx 152 | ```python 153 | from mintersdk.sdk.transactions import MinterRedeemCheckTx 154 | tx = MinterRedeemCheckTx(check='check hash str', proof='proof hash str', nonce=1, gas_coin='SYMBOL') 155 | ``` 156 | 157 | - MinterSellAllCoinTx 158 | ```python 159 | from mintersdk.sdk.transactions import MinterSellAllCoinTx 160 | tx = MinterSellAllCoinTx(coin_to_sell='SYMBOL', coin_to_buy='SYMBOL', min_value_to_buy=100, nonce=1, gas_coin='SYMBOL') 161 | ``` 162 | 163 | - MinterSellCoinTx 164 | ```python 165 | from mintersdk.sdk.transactions import MinterSellCoinTx 166 | tx = MinterSellCoinTx(coin_to_sell='SYMBOL', value_to_sell=1, coin_to_buy='SYMBOL', min_value_to_buy=2, nonce=1, gas_coin='SYMBOL') 167 | ``` 168 | 169 | - MinterSendCoinTx 170 | ```python 171 | from mintersdk.sdk.transactions import MinterSendCoinTx 172 | tx = MinterSendCoinTx(coin='SYMBOL', to='Mx...', value=5, nonce=1, gas_coin='SYMBOL') 173 | ``` 174 | 175 | - MinterMultiSendCoinTx 176 | ```python 177 | from mintersdk.sdk.transactions import MinterMultiSendCoinTx 178 | 179 | txs = [ 180 | {'coin': 'SYMBOL', 'to': 'Mx..1', 'value': 5}, 181 | {'coin': 'SYMBOL', 'to': 'Mx..2', 'value': 1}, 182 | {'coin': 'SYMBOL', 'to': 'Mx..3', 'value': 4} 183 | ] 184 | tx = MinterMultiSendCoinTx(txs=txs, nonce=1, gas_coin='SYMBOL') 185 | ``` 186 | 187 | - MinterSetCandidateOffTx 188 | ```python 189 | from mintersdk.sdk.transactions import MinterSetCandidateOffTx 190 | tx = MinterSetCandidateOffTx(pub_key='Mp...', nonce=1, gas_coin='SYMBOL') 191 | ``` 192 | 193 | - MinterSetCandidateOnTx 194 | ```python 195 | from mintersdk.sdk.transactions import MinterSetCandidateOnTx 196 | tx = MinterSetCandidateOnTx(pub_key='Mp...', nonce=1, gas_coin='SYMBOL') 197 | ``` 198 | 199 | - MinterUnbondTx 200 | ```python 201 | from mintersdk.sdk.transactions import MinterUnbondTx 202 | tx = MinterUnbondTx(pub_key='Mp...', coin='SYMBOL', value=100, nonce=1, gas_coin='SYMBOL') 203 | ``` 204 | 205 | - MinterEditCandidateTx 206 | ```python 207 | from mintersdk.sdk.transactions import MinterEditCandidateTx 208 | tx = MinterEditCandidateTx(pub_key='Mp...', reward_address='Mx...', owner_address='Mx...', nonce=1, gas_coin='SYMBOL') 209 | ``` 210 | 211 | - MinterCreateMultisigTx 212 | ```python 213 | from mintersdk.sdk.transactions import MinterCreateMultisigTx 214 | tx = MinterCreateMultisigTx(threshold=5, weights=[1, 2, 3], addresses=['Mx...', 'Mx...', 'Mx...'], nonce=1, gas_coin='SYMBOL') 215 | ``` 216 | 217 | 218 | ## Sign transaction 219 | When your transaction object is created, you can sign it. 220 | Every transaction can be signed by private key or/and by signature. 221 | Keep in mind, we have some `tx = MinterSomeTx(...)` and API `api = MinterAPI(...)` 222 | 223 | - Sign single signature type transaction 224 | ```python 225 | # Sign with private key 226 | tx.sign(private_key='PRIVATE_KEY') 227 | 228 | # Sign with signature 229 | tx.signature_type = tx.SIGNATURE_SINGLE_TYPE 230 | 231 | signature = tx.generate_signature(private_key='PRIVATE_KEY') 232 | 233 | tx.sign(signature=signature) 234 | ``` 235 | 236 | - Sign multi signature type transaction 237 | ```python 238 | # Sign with private keys 239 | tx.sign(private_key=['PRIVATE_KEY_1', 'PRIVATE_KEY_2', ...], ms_address='Multisig address Mx...') 240 | 241 | # Sign with signatures 242 | tx.signature_type = tx.SIGNATURE_MULTI_TYPE 243 | 244 | signature_1 = tx.generate_signature(private_key='PRIVATE_KEY_1') 245 | signature_2 = tx.generate_signature(private_key='PRIVATE_KEY_2') 246 | 247 | tx.sign(signature=[signature_1, signature_2], ms_address='Multisig address Mx...') 248 | 249 | # Sign with both private keys and signatures 250 | tx.signature_type = tx.SIGNATURE_MULTI_TYPE 251 | 252 | private_key_1 = 'PRIVATE_KEY_1' 253 | private_key_2 = 'PRIVATE_KEY_2' 254 | 255 | signature_1 = tx.generate_signature(private_key='PRIVATE_KEY_3') 256 | signature_2 = tx.generate_signature(private_key='PRIVATE_KEY_4') 257 | 258 | tx.sign(private_key=[private_key_1, private_key_2], signature=[signature_1, signature_2], ms_address='Multisig address Mx...')) 259 | ``` 260 | 261 | As you see above, to generate signature we must set transaction `signature_type` before generating signature. 262 | 263 | You can set this argument while creating transaction. 264 | `tx = MinterSomeTx(..., signature_type=MinterTx.SIGNATURE_MULTI_TYPE)` 265 | `tx = MinterSomeTx(..., signature_type=MinterTx.SIGNATURE_SINGLE_TYPE)` 266 | 267 | After that you can simply generate signature without setting it's signature type by overriding attribute. 268 | `signature = tx.generate_signature(private_key='PRIVATE_KEY')` 269 | 270 | ### Adding signature to multi signature type transaction 271 | When multi signature transaction is created it can be partially signed, e.g. signed by 2 of 3 private keys. 272 | Then partially signed transaction can be transferred to another client and this client can add own signature to transaction. 273 | ```python 274 | from mintersdk.sdk.transactions import MinterTx 275 | 276 | # Client 1 277 | # Create transaction 278 | tx = MinterSomeTx(...) 279 | 280 | # Sign transaction 281 | tx.sign(private_key=['PK_1', 'PK_2'], ms_address='Mx...') 282 | 283 | # Then tx.signed_tx is transferred to Client 2 284 | 285 | 286 | # Client 2 287 | # Received raw_tx (tx.signed_tx from Client 1) 288 | tx = MinterTx.add_signature(signed_tx=raw_tx, private_key='PK_3') 289 | ``` 290 | Client 2 will get new tx object with client's 2 signature. 291 | Client 2 may pass `tx.signed_tx` to next client or just send `tx.signed_tx` to the network. 292 | 293 | 294 | ## Send transaction 295 | When transaction is created and signed, you can send transaction to network. Signed transaction for sending can be found in `tx.signed_tx` attribute. 296 | ```python 297 | # Create transaction 298 | tx = MinterSomeTx(...) 299 | 300 | # Sign transaction 301 | tx.sign(...) 302 | 303 | # Send transaction 304 | response = api.send_transaction(tx=tx.signed_tx) 305 | ``` 306 | 307 | 308 | 309 | # Create transaction from raw 310 | You can create transaction object from raw transaction hash. You will get tx object of tx type. 311 | 312 | ```python 313 | from mintersdk.sdk.transactions import MinterTx 314 | 315 | tx = MinterTx.from_raw(raw_tx='...') 316 | ``` 317 | 318 | 319 | 320 | # Minter deeplink 321 | Let's create a MinterSendCoinTx 322 | ```python 323 | from mintersdk.sdk.transactions import MinterSendCoinTx 324 | tx = MinterSendCoinTx(coin='BIP', to='Mx18467bbb64a8edf890201d526c35957d82be3d95', value=1.23456789, nonce=1, gas_coin='MNT', gas_price=1, payload='Hello World') 325 | ``` 326 | 327 | Now it's time to create deeplink 328 | ```python 329 | from mintersdk.sdk.deeplink import MinterDeeplink 330 | dl = MinterDeeplink(tx=tx, data_only=False) 331 | 332 | # Deeplink is generated by all tx params (nonce, gas_price, gas_coin, payload) by default. 333 | # If you want to create deeplink only by tx data, set `data_only=True` 334 | ``` 335 | 336 | After deeplink object is created, you can override it's attributes, e.g. 337 | ```python 338 | dl = MinterDeeplink(tx=tx) 339 | dl.nonce = '' 340 | dl.gas_coin = 'MNT' 341 | dl.gas_price = 10 342 | ``` 343 | 344 | When your deeplink object is ready, generate it 345 | ```python 346 | url_link = dl.generate() 347 | 348 | # If password check is needed, pass password to generate method 349 | url_link = dl.generate(password='mystrongpassword') 350 | ``` 351 | 352 | Then you might want to create QR-code from your deeplink 353 | ```python 354 | from mintersdk import MinterHelper 355 | qr_code_filepath = MinterHelper.generate_qr(text=url_link) 356 | 357 | # For additional params information for `MinterHelper.generate_qr()`, please see sourcecode for this method. 358 | ``` 359 | 360 | 361 | 362 | # Minter check 363 | ## Create and sign check 364 | ```python 365 | from mintersdk.sdk.check import MinterCheck 366 | 367 | # Create check without password 368 | check = MinterCheck(nonce=1, due_block=300000, coin='MNT', value=1, gas_coin='MNT') 369 | 370 | # Or create check with password 371 | check = MinterCheck(nonce=1, due_block=300000, coin='MNT', value=1, gas_coin='MNT', passphrase='pass') 372 | 373 | # Sign check 374 | signed_check = check.sign(private_key='PRIVATE_KEY') 375 | ``` 376 | 377 | 378 | ## Create proof 379 | ```python 380 | from mintersdk.sdk.check import MinterCheck 381 | 382 | proof = MinterCheck.proof(address='Mx...', passphrase='pass') 383 | ``` 384 | 385 | 386 | ## Create check object from raw 387 | ```python 388 | from mintersdk.sdk.check import MinterCheck 389 | 390 | # Create and sign check 391 | check = MinterCheck(nonce=1, due_block=300000, coin='MNT', value=1, gas_coin='MNT') 392 | signed_check = check.sign(private_key='PRIVATE_KEY') 393 | 394 | # Create object from signed check 395 | check = MinterCheck.from_raw(rawcheck=signed_check) 396 | ``` 397 | 398 | 399 | 400 | # Minter Wallet 401 | ```python 402 | from mintersdk.sdk.wallet import MinterWallet 403 | 404 | # Create new wallet 405 | wallet = MinterWallet.create() 406 | 407 | # Get wallet data from existing mnemonic 408 | wallet = MinterWallet.create(mnemonic='YOUR MNEMONIC PHRASE') 409 | ``` 410 | 411 | 412 | 413 | # Helpers 414 | ## Convert between PIP and BIP 415 | ```python 416 | from mintersdk.shortcuts import to_pip, to_bip 417 | 418 | # Get BIP from PIP 419 | pip_value = 1000000000000000000 420 | bip_value = to_bip(pip_value) 421 | 422 | # Get PIP from BIP 423 | bip_value = 100 424 | pip_value = to_pip(bip_value) 425 | ``` 426 | -------------------------------------------------------------------------------- /UN_logo_on_white.svg: -------------------------------------------------------------------------------- 1 | UN_logo_on_white -------------------------------------------------------------------------------- /mintersdk/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import decimal 4 | import string 5 | import hashlib 6 | import sha3 7 | 8 | import pyqrcode 9 | from deprecated import deprecated 10 | 11 | 12 | # Number of PIP in 1 BIP 13 | PIP = 1000000000000000000 14 | 15 | # Prefixes 16 | PREFIX_ADDR = 'Mx' 17 | PREFIX_PUBKEY = 'Mp' 18 | PREFIX_CHECK = 'Mc' 19 | PREFIX_TX = 'Mt' 20 | 21 | 22 | @deprecated("Use 'to_bip', 'to_pip' shortcuts or MinterHelper methods") 23 | class MinterConvertor: 24 | """ 25 | Class contains different converters 26 | """ 27 | 28 | # PIP in BIP 29 | DEFAULT = 1000000000000000000 30 | 31 | @classmethod 32 | def convert_value(cls, value, to, prec=33): 33 | """ 34 | Convert values from/to pip/bip. 35 | Args: 36 | value (string|int|Decimal|float): value to convert 37 | to (string): coin to convert value to 38 | prec (int): decimal context precision (decimal number length) 39 | Returns: 40 | int|Decimal 41 | """ 42 | # Get default decimal context 43 | context = decimal.getcontext() 44 | # Set temporary decimal context for calculation 45 | decimal.setcontext( 46 | decimal.Context(prec=prec, rounding=decimal.ROUND_DOWN) 47 | ) 48 | 49 | # PIP in BIP in Decimal 50 | default = decimal.Decimal(str(cls.DEFAULT)) 51 | # Value in Decimal 52 | value = decimal.Decimal(str(value)) 53 | 54 | # Make conversion 55 | if to == 'pip': 56 | value = int(value * default) 57 | elif to == 'bip': 58 | value /= default 59 | 60 | # Reset decimal context to default 61 | decimal.setcontext(context) 62 | 63 | return value 64 | 65 | @classmethod 66 | def encode_coin_name(cls, symbol): 67 | """ 68 | Add nulls to coin name 69 | Args: 70 | symbol (string): coin symbol 71 | Returns: 72 | string 73 | """ 74 | return symbol + chr(0) * (10 - len(symbol)) 75 | 76 | @classmethod 77 | def decode_coin_name(cls, symbol): 78 | """ 79 | Args: 80 | symbol (bytes|str) 81 | Returns: 82 | string 83 | """ 84 | 85 | if hasattr(symbol, 'decode'): 86 | symbol = symbol.decode() 87 | 88 | return symbol.replace(chr(0), '') 89 | 90 | 91 | class MinterHelper: 92 | """ 93 | Class which contains different helpers 94 | """ 95 | 96 | @staticmethod 97 | def keccak_hash(data, digest_bits=256): 98 | """ 99 | Create Keccak hash. 100 | Args: 101 | data (bytes) 102 | digest_bits (int) 103 | Returns: 104 | hex (string) 105 | """ 106 | 107 | if digest_bits == 256: 108 | khash = sha3.keccak_256() 109 | else: 110 | raise NotImplementedError 111 | 112 | khash.update(data) 113 | 114 | return khash.hexdigest() 115 | 116 | @staticmethod 117 | @deprecated('Unnecessary method') 118 | def hex2bin(string): 119 | return bytes.fromhex(string) 120 | 121 | @classmethod 122 | def hex2bin_recursive(cls, _dict): 123 | """ 124 | Recursively convert hexdigit dict values to bytes. 125 | Args: 126 | _dict (dict) 127 | Returns: 128 | dict 129 | """ 130 | 131 | def ctype_xdigit(s): 132 | """ 133 | Checks if all of the characters in "s" are hexadecimal 'digits'. 134 | Args: 135 | s (string): string to check 136 | """ 137 | return all(c in string.hexdigits for c in s) 138 | 139 | for k, v in _dict.items(): 140 | if type(v) == dict: 141 | cls.hex2bin_recursive(v) 142 | elif type(v) == str and ctype_xdigit(v): 143 | try: 144 | _dict[k] = bytes.fromhex(v) 145 | except ValueError: 146 | pass 147 | 148 | return _dict 149 | 150 | @staticmethod 151 | @deprecated('Unnecessary method') 152 | def bin2hex(bts): 153 | return bts.hex() 154 | 155 | @staticmethod 156 | @deprecated('Unnecessary method') 157 | def bin2int(number): 158 | return int.from_bytes(number, 'big') 159 | 160 | @staticmethod 161 | def get_validator_address(pub_key, upper=True): 162 | """ 163 | Get validator address from it's pub key (Mp...). 164 | Validator address is used in signing blocks. 165 | Args: 166 | pub_key (string): candidate public key (Mp....) 167 | upper (bool) 168 | Returns: 169 | string, validator address 170 | """ 171 | 172 | pub_key = bytes.fromhex(MinterHelper.prefix_remove(pub_key)) 173 | vaddress = hashlib.sha256(pub_key).hexdigest()[:40] 174 | 175 | return vaddress.upper() if upper else vaddress 176 | 177 | @staticmethod 178 | def generate_qr(text, fn=None, path='', error='H', version=None, mode=None, 179 | output='svg', module_color='black', background='white', 180 | quiet_zone=4): 181 | """ 182 | Generate QR code from text and save to file. 183 | Detailed documentation for `pyqrcode` package can be found 184 | here: https://pythonhosted.org/PyQRCode/index.html 185 | Args: 186 | text (str): Text, that should be encoded to QR 187 | fn (str): Filename for generated QR. 188 | If not provided random filename is generated. 189 | path (str): Path to save generate QR 190 | error (str|int): The error parameter sets the error correction 191 | level of the code. 192 | Each level has an associated name given by a 193 | letter: L, M, Q, or H; 194 | each level can correct up to 7, 15, 25, or 30 195 | percent of the data respectively. 196 | version (int): The version parameter specifies the size and data 197 | capacity of the code. 198 | Versions are any integer between 1 and 40 199 | mode (str): The mode param sets how the contents will be encoded. 200 | Three of the four possible encodings are available. 201 | By default, the object uses the most efficient 202 | encoding for the contents. You can override this 203 | behavior by setting this parameter. 204 | output (str): Render modes. Available: text|terminal|svg. 205 | In `text`|`terminal` modes QR code is printed, 206 | `svg` mode saves QR code to file `fn` to path `path`. 207 | module_color (str): String color of QR code data. 208 | Is used only for `terminal` and `svg` modes. 209 | background (str): String color of QR code background. 210 | Is used only for `terminal` and `svg` modes. 211 | quiet_zone (int): QR code quiet zone. 212 | Returns: 213 | fnpath (str): Path to generated QR 214 | """ 215 | 216 | # Generate QR code object 217 | qrcode = pyqrcode.create(content=text, error=error, version=version, 218 | mode=mode) 219 | 220 | # Render QR code depending on `output` param 221 | if output == 'text': 222 | print(qrcode.text(quiet_zone=quiet_zone)) 223 | elif output == 'terminal': 224 | print( 225 | qrcode.terminal( 226 | module_color=module_color, background=background, 227 | quiet_zone=quiet_zone 228 | ) 229 | ) 230 | elif output == 'svg': 231 | # Generate filename, if not provided 232 | if not fn: 233 | fn = text + str(random.randint(10000, 99999)) 234 | fn = hashlib.sha256(fn.encode()).hexdigest()[:10] 235 | fnpath = os.path.join(path, fn + '.svg') 236 | 237 | # Save QR code to file 238 | qrcode.svg(file=fnpath, module_color=module_color, 239 | background=background, quiet_zone=quiet_zone) 240 | 241 | return fnpath 242 | else: 243 | raise Exception('Wrong QR code render mode') 244 | 245 | @staticmethod 246 | def bytes_len(value, encoding='utf-8'): 247 | """ 248 | Count bytes length 249 | Args: 250 | value (str|bytes) 251 | encoding (str) 252 | """ 253 | if type(value) is str: 254 | value = bytes(value, encoding=encoding) 255 | 256 | return len(value) 257 | 258 | @staticmethod 259 | def encode_coin_name(symbol): 260 | """ 261 | Add nulls to coin name 262 | Args: 263 | symbol (string): coin symbol 264 | Returns: 265 | string 266 | """ 267 | return symbol + chr(0) * (10 - len(symbol)) 268 | 269 | @staticmethod 270 | def decode_coin_name(symbol): 271 | """ 272 | Args: 273 | symbol (bytes|str) 274 | Returns: 275 | string 276 | """ 277 | 278 | if hasattr(symbol, 'decode'): 279 | symbol = symbol.decode() 280 | 281 | return symbol.replace(chr(0), '') 282 | 283 | @staticmethod 284 | def to_pip(value): 285 | """ 286 | Convert BIPs to PIPs. 287 | Always cast value to str, due to float behaviour: 288 | Decimal(0.1) = Decimal('0.10000000000004524352345234') 289 | Decimal('0.1') = Decimal('0.1') 290 | Args: 291 | value (str|float|int|Decimal): value in BIP 292 | Returns: 293 | int 294 | """ 295 | return int(decimal.Decimal(str(value)) * decimal.Decimal(PIP)) 296 | 297 | @staticmethod 298 | def to_bip(value): 299 | """ 300 | Convert PIPs to BIPs. 301 | Use dynamic Decimal precision, depending on value length. 302 | Args: 303 | value (int|str|Decimal): value in PIP 304 | Returns: 305 | Decimal 306 | """ 307 | # Check if value is correct PIP value 308 | value = str(value) 309 | if not value.isdigit(): 310 | raise ValueError(f'{value} is not correct PIP value') 311 | 312 | # Get default decimal context 313 | context = decimal.getcontext() 314 | # Set temporary decimal context for calculation 315 | decimal.setcontext( 316 | decimal.Context(prec=len(value), rounding=decimal.ROUND_DOWN) 317 | ) 318 | 319 | # Convert value 320 | value = decimal.Decimal(value) / decimal.Decimal(PIP) 321 | 322 | # Reset decimal context to default 323 | decimal.setcontext(context) 324 | 325 | return value 326 | 327 | @staticmethod 328 | def prefix_add(value, prefix): 329 | if prefix not in [PREFIX_ADDR, PREFIX_PUBKEY, PREFIX_CHECK, PREFIX_TX]: 330 | raise ValueError(f"Unknown prefix '{prefix}'") 331 | return prefix + value 332 | 333 | @staticmethod 334 | def prefix_remove(value): 335 | value = value.replace(PREFIX_ADDR, '') 336 | value = value.replace(PREFIX_PUBKEY, '') 337 | value = value.replace(PREFIX_CHECK, '') 338 | value = value.replace(PREFIX_TX, '') 339 | 340 | return value 341 | 342 | 343 | @deprecated("Deprecated. Use 'MinterHelper' class instead") 344 | class MinterPrefix: 345 | """ 346 | Class with minter prefixes and operations with them. 347 | """ 348 | 349 | # Minter wallet address prefix 350 | ADDRESS = 'Mx' 351 | 352 | # Minter public key prefix 353 | PUBLIC_KEY = 'Mp' 354 | 355 | # Minter redeem check prefix 356 | CHECK = 'Mc' 357 | 358 | # Minter transaction prefix 359 | TRANSACTION = 'Mt' 360 | 361 | @staticmethod 362 | def remove_prefix(string, prefix): 363 | return string.replace(prefix, '') 364 | -------------------------------------------------------------------------------- /mintersdk/minterapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Roman 3 | """ 4 | import requests 5 | import json 6 | import base64 7 | 8 | from deprecated import deprecated 9 | from mintersdk import MinterHelper 10 | 11 | 12 | class MinterAPI(object): 13 | """ 14 | Base MinterAPI class 15 | """ 16 | # API host 17 | api_url = '' 18 | 19 | # Timeout connecting to host 20 | connect_timeout = 1 21 | 22 | # Timeout reading from host 23 | read_timeout = 3 24 | 25 | # Default request headers 26 | headers = { 27 | 'Content-Type': 'application/json' 28 | } 29 | 30 | def __init__(self, api_url, **kwargs): 31 | """ 32 | Args: 33 | api_url (str): API host, e.g. http://localhost/api/ 34 | kwargs: Any other attributes you need 35 | Predefined kwargs: 36 | - connect_timeout (float|int) 37 | - read_timeout (float|int) 38 | - headers (dict) 39 | 40 | """ 41 | self.api_url = api_url 42 | if self.api_url[-1] != '/': 43 | self.api_url += '/' 44 | 45 | for name, value in kwargs.items(): 46 | setattr(self, name, value) 47 | 48 | def get_status(self): 49 | """ Get node status """ 50 | return self._request(command='status') 51 | 52 | def get_candidate(self, public_key, height=None, pip2bip=False): 53 | """ 54 | Get candidate 55 | Args: 56 | public_key (string): candidate public key 57 | height (int): block height, 58 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 59 | """ 60 | response = self._request( 61 | command='candidate', 62 | params={'pub_key': public_key, 'height': height} 63 | ) 64 | 65 | if pip2bip: 66 | return self.__response_processor( 67 | data=response, 68 | funcs=[(self.__pip_to_bip, {'exclude': ['commission']})], 69 | ) 70 | 71 | return response 72 | 73 | def get_validators(self, height=None, page=None, limit=None): 74 | """ 75 | Get validators list 76 | Args: 77 | height (int): get validators on specified block height 78 | page (int|None): page number 79 | limit (int|None): items per page 80 | """ 81 | return self._request( 82 | command='validators', 83 | params={'height': height, 'page': page, 'perPage': limit} 84 | ) 85 | 86 | def get_addresses(self, addresses, height=None, pip2bip=False): 87 | """ 88 | Returns addresses balances 89 | Args: 90 | addresses (list[str]): Addresses list 91 | height (int|None): Block height 92 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 93 | """ 94 | response = self._request( 95 | command='addresses', 96 | params={'addresses': json.dumps(addresses), 'height': height} 97 | ) 98 | 99 | if pip2bip: 100 | return self.__response_processor( 101 | data=response, funcs=[self.__pip_to_bip] 102 | ) 103 | 104 | return response 105 | 106 | def get_balance(self, address, height=None, pip2bip=False): 107 | """ 108 | Get balance by address 109 | Args: 110 | address (string): wallet address 111 | height (int): block height 112 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 113 | """ 114 | response = self._request( 115 | command='address', params={'address': address, 'height': height} 116 | ) 117 | 118 | if pip2bip: 119 | return self.__response_processor( 120 | data=response, funcs=[self.__pip_to_bip] 121 | ) 122 | 123 | return response 124 | 125 | def get_nonce(self, address): 126 | """ 127 | Nonce - int, used for prevent transaction reply 128 | Args: 129 | address (string): wallet address 130 | """ 131 | 132 | balance = self.get_balance(address) 133 | nonce = balance['result']['transaction_count'] + 1 134 | 135 | return nonce 136 | 137 | def send_transaction(self, tx): 138 | """ 139 | Send transaction 140 | Args: 141 | tx (string): signed transaction to send 142 | """ 143 | return self._request( 144 | command='send_transaction', params={'tx': '0x' + tx} 145 | ) 146 | 147 | def get_transaction(self, tx_hash, pip2bip=False, decode_payload=False): 148 | """ 149 | Get transaction info 150 | Args: 151 | tx_hash (string): transaction hash 152 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 153 | decode_payload (bool): Try to decode payload from base64 154 | """ 155 | response = self._request( 156 | command='transaction', params={'hash': '0x' + tx_hash} 157 | ) 158 | 159 | # Convert PIPs to BIPs 160 | if pip2bip: 161 | response = self.__response_processor( 162 | data=response, 163 | funcs=[(self.__pip_to_bip, {'exclude': ['commission']})] 164 | ) 165 | 166 | # Decode payload 167 | if decode_payload: 168 | response['result']['payload'] = self._decode_payload( 169 | payload=response['result']['payload'] 170 | ) 171 | 172 | return response 173 | 174 | def get_block(self, height, pip2bip=False): 175 | """ 176 | Get block data at given height 177 | Args: 178 | height (int): block height 179 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 180 | """ 181 | response = self._request(command='block', params={'height': height}) 182 | 183 | if pip2bip: 184 | return self.__response_processor( 185 | data=response, funcs=[self.__pip_to_bip] 186 | ) 187 | 188 | return response 189 | 190 | def get_latest_block_height(self): 191 | """ 192 | Get latest block height 193 | """ 194 | return self.get_status()['result']['latest_block_height'] 195 | 196 | def get_events(self, height, pip2bip=False): 197 | """ 198 | Get events at given height 199 | Args: 200 | height (int): block height 201 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 202 | """ 203 | response = self._request(command='events', params={'height': height}) 204 | 205 | if pip2bip: 206 | return self.__response_processor( 207 | data=response, funcs=[self.__pip_to_bip] 208 | ) 209 | 210 | return response 211 | 212 | def get_candidates(self, height=None, include_stakes=False, pip2bip=False): 213 | """ 214 | Get candidates 215 | Args: 216 | height (int): block height 217 | include_stakes (bool) 218 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 219 | """ 220 | response = self._request( 221 | command='candidates', 222 | params={ 223 | 'height': height, 224 | 'include_stakes': str(include_stakes).lower() 225 | } 226 | ) 227 | 228 | if pip2bip: 229 | return self.__response_processor( 230 | data=response, 231 | funcs=[(self.__pip_to_bip, {'exclude': ['commission']})] 232 | ) 233 | 234 | return response 235 | 236 | def get_coin_info(self, symbol, height=None, pip2bip=False): 237 | """ 238 | Get information about coin 239 | Args: 240 | symbol (string): coin name 241 | height (int): block height 242 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 243 | """ 244 | response = self._request( 245 | command='coin_info', 246 | params={'symbol': symbol.upper(), 'height': height} 247 | ) 248 | 249 | if pip2bip: 250 | return self.__response_processor( 251 | data=response, funcs=[self.__pip_to_bip] 252 | ) 253 | 254 | return response 255 | 256 | def estimate_coin_sell(self, coin_to_sell, value_to_sell, coin_to_buy, 257 | height=None, pip2bip=False): 258 | """ 259 | Return estimate of sell coin transaction 260 | Args: 261 | coin_to_sell (string): coin name to sell 262 | value_to_sell (string|int): Amount of coins to sell in PIP. 263 | Provide `value_to_sell` in PIP, if `pip2bip` False. 264 | Provide `value_to_sell` in BIP, if `pip2bip` True. 265 | coin_to_buy (string): coin name to buy 266 | height (int): block height 267 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 268 | """ 269 | # Convert `value_to_sell` to PIP, if needed 270 | if pip2bip: 271 | value_to_sell = MinterHelper.to_pip(value_to_sell) 272 | 273 | # Get default response 274 | response = self._request( 275 | command='estimate_coin_sell', 276 | params={ 277 | 'coin_to_sell': coin_to_sell.upper(), 278 | 'value_to_sell': value_to_sell, 279 | 'coin_to_buy': coin_to_buy.upper(), 280 | 'height': height 281 | } 282 | ) 283 | 284 | # Convert response values from PIP to BIP, if needed 285 | if pip2bip: 286 | return self.__response_processor( 287 | data=response, funcs=[self.__pip_to_bip] 288 | ) 289 | 290 | return response 291 | 292 | def estimate_coin_sell_all(self, coin_to_sell, value_to_sell, coin_to_buy, 293 | height=None, pip2bip=False): 294 | """ 295 | Return estimate of sell all coin transaction. 296 | Args: 297 | coin_to_sell (string): coin name to sell 298 | value_to_sell (string|int): Amount of coins to sell in PIP. 299 | Provide `value_to_sell` in PIP, if `pip2bip` False. 300 | Provide `value_to_sell` in BIP, if `pip2bip` True. 301 | coin_to_buy (string): coin name to buy 302 | height (int): block height 303 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 304 | """ 305 | # Convert `value_to_sell` to PIP, if needed 306 | if pip2bip: 307 | value_to_sell = MinterHelper.to_pip(value_to_sell) 308 | 309 | # Get default response 310 | response = self._request( 311 | command='estimate_coin_sell_all', 312 | params={ 313 | 'coin_to_sell': coin_to_sell.upper(), 314 | 'value_to_sell': value_to_sell, 315 | 'coin_to_buy': coin_to_buy.upper(), 316 | 'height': height 317 | } 318 | ) 319 | 320 | # Convert response values from PIP to BIP, if needed 321 | if pip2bip: 322 | return self.__response_processor( 323 | data=response, funcs=[self.__pip_to_bip] 324 | ) 325 | 326 | return response 327 | 328 | def estimate_coin_buy(self, coin_to_sell, value_to_buy, coin_to_buy, 329 | height=None, pip2bip=False): 330 | """ 331 | Return estimate of buy coin transaction 332 | Args: 333 | coin_to_sell (string): coin name to sell 334 | value_to_buy (string): Amount of coins to buy in PIP. 335 | Provide `value_to_buy` in PIP, if `pip2bip` False. 336 | Provide `value_to_buy` in BIP, if `pip2bip` True. 337 | coin_to_buy (string): coin name to buy 338 | height (int): block height 339 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 340 | """ 341 | # Convert `value_to_buy` to PIP, if needed 342 | if pip2bip: 343 | value_to_buy = MinterHelper.to_pip(value_to_buy) 344 | 345 | # Get default response 346 | response = self._request( 347 | command='estimate_coin_buy', 348 | params={ 349 | 'coin_to_sell': coin_to_sell, 350 | 'value_to_buy': value_to_buy, 351 | 'coin_to_buy': coin_to_buy, 352 | 'height': height 353 | } 354 | ) 355 | 356 | # Convert response values from PIP to BIP, if needed 357 | if pip2bip: 358 | return self.__response_processor( 359 | data=response, funcs=[self.__pip_to_bip] 360 | ) 361 | 362 | return response 363 | 364 | @deprecated("Please, use 'estimate_tx_commission' instead") 365 | def estimate_tx_comission(self, tx, height=None): 366 | """ 367 | Estimate current tx gas. 368 | Args: 369 | tx (string): signed transaction 370 | height (int|None): block height 371 | """ 372 | if tx[:2] != '0x': 373 | tx = '0x' + tx 374 | 375 | return self._request( 376 | command='estimate_tx_commission', 377 | params={'tx': tx, 'height': height} 378 | ) 379 | 380 | def estimate_tx_commission(self, tx, height=None, pip2bip=False): 381 | """ 382 | Estimate current tx gas. 383 | Args: 384 | tx (string): signed transaction 385 | height (int|None): block height 386 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 387 | """ 388 | if tx[:2] != '0x': 389 | tx = '0x' + tx 390 | 391 | response = self._request( 392 | command='estimate_tx_commission', 393 | params={'tx': tx, 'height': height} 394 | ) 395 | 396 | if pip2bip: 397 | return self.__response_processor( 398 | data=response, funcs=[self.__pip_to_bip] 399 | ) 400 | 401 | return response 402 | 403 | def get_transactions(self, query, page=None, limit=None, pip2bip=False, 404 | decode_payload=False): 405 | """ 406 | Get transactions by query. 407 | Args: 408 | query (string) 409 | page (int) 410 | limit (int) 411 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 412 | decode_payload (bool): Try to decode payload 413 | """ 414 | response = self._request( 415 | command='transactions', 416 | params={'query': query, 'page': page, 'perPage': limit} 417 | ) 418 | 419 | # Convert PIPs to BIPs 420 | if pip2bip: 421 | response = self.__response_processor( 422 | data=response, funcs=[self.__pip_to_bip] 423 | ) 424 | 425 | # Decode payload 426 | if decode_payload: 427 | for item in response['result']: 428 | item['payload'] = self._decode_payload(payload=item['payload']) 429 | 430 | return response 431 | 432 | def get_unconfirmed_transactions(self, limit=None): 433 | """ 434 | Get unconfirmed transactions. 435 | Args: 436 | limit (int) 437 | """ 438 | return self._request( 439 | command='unconfirmed_txs', params={'limit': limit} 440 | ) 441 | 442 | def get_max_gas_price(self, height=None): 443 | """ 444 | Returns current max gas price. 445 | Args: 446 | height (int) 447 | """ 448 | return self._request(command='max_gas', params={'height': height}) 449 | 450 | def get_min_gas_price(self): 451 | """ 452 | Returns min gas price. 453 | """ 454 | return self._request(command='min_gas_price') 455 | 456 | def get_missed_blocks(self, public_key, height=None): 457 | """ 458 | Returns missed blocks by validator public key. 459 | Args: 460 | public_key (str): candidate public key 461 | height (int): block chain height 462 | """ 463 | return self._request( 464 | command='missed_blocks', 465 | params={'pub_key': public_key, 'height': height} 466 | ) 467 | 468 | def get_genesis(self, pip2bip=False): 469 | """ 470 | Return network genesis. 471 | Args: 472 | pip2bip (bool): Convert coin amounts to BIP (default is in PIP) 473 | """ 474 | response = self._request(command='genesis') 475 | if pip2bip: 476 | return self.__response_processor( 477 | data=response, funcs=[self.__pip_to_bip] 478 | ) 479 | return response 480 | 481 | def get_network_info(self): 482 | """ Return node network information. """ 483 | return self._request(command='net_info') 484 | 485 | def _request(self, command, request_type='get', **kwargs): 486 | """ 487 | Send all requests to API 488 | Args: 489 | command (str): API command 490 | request_type (str): Request type (GET|POST) 491 | kwargs: requests package arguments 492 | """ 493 | # Add timeouts if were not set 494 | if not kwargs.get('timeout', None): 495 | kwargs['timeout'] = (self.connect_timeout, self.read_timeout) 496 | 497 | # Add headers 498 | if not kwargs.get('headers', None): 499 | kwargs['headers'] = self.headers 500 | 501 | # Trying make request 502 | try: 503 | url = self.api_url + command 504 | 505 | if request_type == 'get': 506 | response = requests.get(url, **kwargs) 507 | elif request_type == 'post': 508 | response = requests.post(url, **kwargs) 509 | else: 510 | response = None 511 | 512 | # Try to get json response and prepare result 513 | try: 514 | return self.__response_processor( 515 | data=response.json(), funcs=[self.__digits_to_int] 516 | ) 517 | except Exception as e: 518 | msg = 'Response parse JSON error: {}; Response is: {}' 519 | raise Exception(msg.format(e.__str__(), response.text)) 520 | except requests.exceptions.ReadTimeout: 521 | raise 522 | except requests.exceptions.ConnectTimeout: 523 | raise 524 | except requests.exceptions.ConnectionError: 525 | raise 526 | except requests.exceptions.HTTPError: 527 | raise 528 | except ValueError: 529 | raise 530 | 531 | @staticmethod 532 | def __digits_to_int(value, key, exclude=None): 533 | """ 534 | Numeric strings to integers converter. 535 | Used as processor function for '__response_processor()' 536 | Args: 537 | exclude (list|None): keys to be excluded 538 | Returns: 539 | int|any 540 | """ 541 | # Combine default exclude list with argument 542 | exclude_keys = ['tx.type'] 543 | if type(exclude) is list: 544 | exclude_keys += exclude 545 | 546 | # Convert value 547 | if type(value) is str and value.isdigit() and \ 548 | (key not in exclude_keys or key is None): 549 | return int(value) 550 | 551 | return value 552 | 553 | @staticmethod 554 | def __pip_to_bip(value, key, exclude=None): 555 | """ 556 | Convert coin amounts integers from PIP to BIP. 557 | Used as processor function for '__response_processor()' 558 | Args: 559 | exclude ([str]): Keys excluded from conversion 560 | Returns: 561 | Decimal|int 562 | """ 563 | # Keys with coin values. Keys' values should be converted 564 | # from PIP to BIP 565 | include_keys = [ 566 | 'total_stake', 'value', 'bip_value', 'value_to_buy', 'volume', 567 | 'maximum_value_to_sell', 'initial_amount', 'initial_reserve', 568 | 'max_supply', 'stake', 'value_to_sell', 'tx.sell_amount', 569 | 'minimum_value_to_buy', 'tx.return', 'block_reward', 'amount', 570 | 'reserve_balance', 'will_get', 'commission', 'total_bip_stake', 571 | 'will_pay', 'accum_reward', 'total_slashed' 572 | ] 573 | 574 | # Combine default exclude list with argument 575 | exclude_keys = [] 576 | if type(exclude) is list: 577 | exclude_keys += exclude 578 | 579 | # Convert value. 580 | # Key should be present in 'include_keys' and should be missing 581 | # in 'exclude_keys' or key is coin symbol. 582 | # DANGEROUS! We determine if key is coin symbol by checking 583 | # if key is uppercase or not (should think more about it). 584 | if type(value) is int and \ 585 | ( 586 | (key in include_keys and key not in exclude_keys) or 587 | key.isupper() 588 | ): 589 | return MinterHelper.to_bip(value) 590 | 591 | return value 592 | 593 | @staticmethod 594 | def __response_processor(data, funcs): 595 | """ 596 | Works only with successful response: response status is 200 597 | and contains 'result' key. 598 | Args: 599 | data (dict): Response text dict (json parsed) 600 | funcs (list): Functions to be applied to dict values 601 | Returns: 602 | dict 603 | """ 604 | 605 | def data_recursive(result, fn): 606 | """ 607 | Method is used for looping response 'result' until plain 608 | values are got. 609 | Then this values are processed by 'fn'. 610 | Args: 611 | result (any): 'result' key value from response text 612 | fn (function|[function, kwargs]): function to process value 613 | Returns: 614 | result (any) 615 | """ 616 | # Unpack fn if list or tuple 617 | processor = fn 618 | kwargs = {} 619 | if type(fn) in [list, tuple]: 620 | processor, kwargs = fn 621 | 622 | # Go recursion 623 | if type(result) is list: 624 | for item in result: 625 | data_recursive(item, fn) 626 | elif type(result) is dict: 627 | for key, value in result.items(): 628 | if type(value) in [list, dict]: 629 | data_recursive(value, fn) 630 | else: 631 | result[key] = processor(value, key, **kwargs) 632 | else: 633 | return processor(value=result, key=None, **kwargs) 634 | 635 | return result 636 | 637 | # If there is no 'result' key in data, return original data 638 | if not data.get('result'): 639 | return data 640 | 641 | # Otherwise apply value processor functions to 'result' key values 642 | for func in funcs: 643 | data['result'] = data_recursive(result=data['result'], fn=func) 644 | 645 | return data 646 | 647 | @staticmethod 648 | def _decode_payload(payload): 649 | """ Decode payload from base64 and try get string """ 650 | if payload: 651 | try: 652 | payload = base64.b64decode(payload) 653 | except Exception: 654 | return payload 655 | 656 | try: 657 | return payload.decode() 658 | except Exception: 659 | return payload 660 | 661 | return payload 662 | -------------------------------------------------------------------------------- /mintersdk/sdk/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Roman Matusevich 3 | """ 4 | import sslcrypto 5 | 6 | 7 | class ECDSA: 8 | """ 9 | ECDSA class 10 | """ 11 | 12 | # Curve data 13 | curve = sslcrypto.ecc.get_curve('secp256k1') 14 | pub_key_len = curve._backend.public_key_length 15 | 16 | @classmethod 17 | def sign(cls, message, private_key): 18 | """ 19 | Args: 20 | message (string): message to sign 21 | private_key (string): private_key 22 | Returns: 23 | list(int) 24 | """ 25 | 26 | # Create signature 27 | signature = cls.curve.sign( 28 | data=bytes.fromhex(message), private_key=bytes.fromhex(private_key), 29 | recoverable=True, hash=None 30 | ) 31 | v = signature[0] 32 | r = int.from_bytes(signature[1:cls.pub_key_len+1], byteorder='big') 33 | s = int.from_bytes(signature[cls.pub_key_len+1:], byteorder='big') 34 | 35 | return [v, r, s] 36 | 37 | @classmethod 38 | def recover(cls, message, vrs): 39 | """ 40 | Args: 41 | message (string): message 42 | vrs (tuple): tuple of v, r, s (r, s in hex) 43 | Returns: 44 | str 45 | """ 46 | 47 | # Create signature 48 | signature = ( 49 | vrs[0].to_bytes(length=1, byteorder='big') + 50 | int(vrs[1], 16).to_bytes( 51 | length=cls.pub_key_len, byteorder='big' 52 | ) + 53 | int(vrs[2], 16).to_bytes( 54 | length=cls.pub_key_len, byteorder='big' 55 | ) 56 | ) 57 | 58 | # Get raw recover of public key. 59 | pub_key_raw = cls.curve.recover( 60 | signature=signature, data=bytes.fromhex(message), hash=None 61 | ) 62 | 63 | # Convert public key to hex electrum format 64 | x, y = cls.curve.decode_public_key(pub_key_raw) 65 | pub_key = x.hex() + y.hex() 66 | 67 | return pub_key 68 | -------------------------------------------------------------------------------- /mintersdk/sdk/check.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Roman Matusevich 3 | """ 4 | import hashlib 5 | 6 | import rlp 7 | from mintersdk import MinterHelper, PREFIX_CHECK, PREFIX_PUBKEY 8 | from mintersdk.sdk import ECDSA 9 | from mintersdk.sdk.wallet import MinterWallet 10 | 11 | 12 | class MinterCheck(object): 13 | """ 14 | Minter check class 15 | Create new check or decode existing 16 | """ 17 | 18 | def __init__(self, nonce, due_block, coin, value, gas_coin, 19 | passphrase='', chain_id=1, **kwargs): 20 | """ 21 | Args: 22 | nonce (int) 23 | due_block (int) 24 | coin (str) 25 | value (float) 26 | gas_coin (str): Gas coin symbol 27 | passphrase (str) 28 | chain_id (int) 29 | """ 30 | 31 | self.nonce = nonce 32 | self.due_block = due_block 33 | self.coin = coin.upper() 34 | self.value = value 35 | self.gas_coin = gas_coin.upper() 36 | self.passphrase = passphrase 37 | self.chain_id = chain_id 38 | 39 | for attr, value in kwargs.items(): 40 | setattr(self, attr, value) 41 | 42 | @staticmethod 43 | def __hash(data): 44 | """ 45 | Create service hash by RLP encoding and getting keccak hash from 46 | rlp result 47 | Args: 48 | data (list) 49 | Returns: 50 | hash (str) 51 | """ 52 | return MinterHelper.keccak_hash(rlp.encode(data)) 53 | 54 | @staticmethod 55 | def __lockfromsignature(signature): 56 | """ 57 | Create lock from signature 58 | Args: 59 | signature (list): [v, r, s] list 60 | Returns: 61 | bytes 62 | """ 63 | 64 | v, r, s = signature 65 | v = '00' if v == 27 else '01' 66 | signature = format(r, 'x').zfill(64) + format(s, 'x').zfill(64) + v 67 | 68 | return bytes.fromhex(signature) 69 | 70 | def sign(self, private_key): 71 | """ 72 | Sign check 73 | Args: 74 | private_key (str) 75 | """ 76 | # Prepare structure 77 | # It contains nonce, chain_id, due_block, coin, value, gas_coin, 78 | # lock, v, r, s. 79 | # lock, v, r, s appended later in code 80 | structure = [ 81 | int(str(self.nonce).encode().hex(), 16), 82 | self.chain_id, 83 | self.due_block, 84 | MinterHelper.encode_coin_name(self.coin), 85 | MinterHelper.to_pip(self.value), 86 | MinterHelper.encode_coin_name(self.gas_coin) 87 | ] 88 | 89 | # Create msg hash 90 | msg_hash = self.__hash(structure) 91 | 92 | # SHA256 from passphrase 93 | sha = hashlib.sha256() 94 | sha.update(self.passphrase.encode()) 95 | passphrase = sha.hexdigest() 96 | 97 | # Create lock from signature 98 | self.lock = self.__lockfromsignature( 99 | signature=ECDSA.sign(message=msg_hash, private_key=passphrase) 100 | ) 101 | 102 | # Re-create msg hash with adding lock to structure 103 | structure.append(self.lock) 104 | msg_hash = self.__hash(structure) 105 | 106 | # Re-create signature, add it to check attrs and to structure 107 | signature = ECDSA.sign(message=msg_hash, private_key=private_key) 108 | self.signature = { 109 | 'v': signature[0], 110 | 'r': format(signature[1], 'x'), 111 | 's': format(signature[2], 'x') 112 | } 113 | structure += signature 114 | 115 | # Get RLP, which will be the check 116 | check = rlp.encode(structure).hex() 117 | 118 | return MinterHelper.prefix_add(check, PREFIX_CHECK) 119 | 120 | @classmethod 121 | def proof(cls, address, passphrase=''): 122 | """ 123 | Create proof 124 | Args: 125 | address (str) 126 | passphrase (str) 127 | Returns: 128 | str 129 | """ 130 | 131 | # Get address hash 132 | address = MinterHelper.prefix_remove(address) 133 | address = bytes.fromhex(address) 134 | address_hash = cls.__hash(data=[address]) 135 | 136 | # Create SHA256 from passphrase 137 | sha = hashlib.sha256() 138 | sha.update(passphrase.encode()) 139 | passphrase = sha.hexdigest() 140 | 141 | # Get signature 142 | signature = ECDSA.sign(message=address_hash, private_key=passphrase) 143 | 144 | return cls.__lockfromsignature(signature).hex() 145 | 146 | @classmethod 147 | def from_raw(cls, rawcheck): 148 | """ 149 | Create check instance from raw check 150 | Args: 151 | rawcheck (str) 152 | Returns: 153 | MinterCheck 154 | """ 155 | 156 | # Remove check prefix and RLP decode it 157 | rawcheck = MinterHelper.prefix_remove(rawcheck) 158 | rawcheck = bytes.fromhex(rawcheck) 159 | decoded = rlp.decode(rawcheck) 160 | 161 | # Create MinterCheck instance 162 | kwargs = { 163 | 'nonce': int(decoded[0].decode()), 164 | 'chain_id': int.from_bytes(decoded[1], 'big'), 165 | 'due_block': int.from_bytes(decoded[2], 'big'), 166 | 'coin': MinterHelper.decode_coin_name(decoded[3]), 167 | 'value': MinterHelper.to_bip(int.from_bytes(decoded[4], 'big')), 168 | 'gas_coin': MinterHelper.decode_coin_name(decoded[5]), 169 | 'lock': decoded[6].hex(), 170 | 'signature': { 171 | 'v': int.from_bytes(decoded[7], 'big'), 172 | 'r': decoded[8].hex(), 173 | 's': decoded[9].hex() 174 | } 175 | } 176 | check = MinterCheck(**kwargs) 177 | 178 | # Recover owner address 179 | msg_hash = cls.__hash(data=[ 180 | int(str(check.nonce).encode().hex(), 16), 181 | check.chain_id, 182 | check.due_block, 183 | MinterHelper.encode_coin_name(check.coin), 184 | MinterHelper.to_pip(check.value), 185 | MinterHelper.encode_coin_name(check.gas_coin), 186 | bytes.fromhex(check.lock) 187 | ]) 188 | public_key = ECDSA.recover(msg_hash, tuple(check.signature.values())) 189 | public_key = MinterHelper.prefix_add(public_key, PREFIX_PUBKEY) 190 | 191 | check.owner = MinterWallet.get_address_from_public_key(public_key) 192 | 193 | return check 194 | -------------------------------------------------------------------------------- /mintersdk/sdk/deeplink.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Roman Matusevich 3 | """ 4 | import base64 5 | 6 | import rlp 7 | from mintersdk import MinterHelper 8 | 9 | 10 | class MinterDeeplink(object): 11 | """ Create deeplink for Minter transaction """ 12 | 13 | BASE_URL = 'https://bip.to/tx' 14 | 15 | def __init__(self, tx, data_only=False, base_url=BASE_URL): 16 | """ 17 | Create deeplink object 18 | Args: 19 | tx (MinterTx): MinterTx object 20 | data_only (bool): Generate deeplink only with tx data 21 | base_url (str): Base URL for generated deeplink 22 | """ 23 | super(MinterDeeplink, self).__init__() 24 | 25 | self.base_url = base_url 26 | 27 | # Generate deeplink structure attributes 28 | self.__type = tx.TYPE 29 | self.__data = self.__get_tx_data(tx=tx) 30 | self.nonce = tx.nonce if not data_only else '' 31 | self.gas_price = tx.gas_price if not data_only else '' 32 | self.gas_coin = tx.gas_coin if not data_only else '' 33 | self.payload = tx.payload if not data_only else '' 34 | 35 | @staticmethod 36 | def __get_tx_data(tx): 37 | """ Get data from transaction """ 38 | structure = tx._structure_from_instance() 39 | return rlp.encode(list(structure['data'].values())) 40 | 41 | def generate(self, password=None): 42 | """ 43 | Generate deeplink 44 | Args: 45 | password (str): Check password 46 | Returns: 47 | deeplink (str) 48 | """ 49 | 50 | # Create deeplink structure 51 | gas_coin = MinterHelper.encode_coin_name( 52 | self.gas_coin 53 | ) if self.gas_coin else '' 54 | deep_structure = [self.__type, self.__data, self.payload, self.nonce, 55 | self.gas_price, gas_coin] 56 | 57 | # Create deephash base64 urlsafe 58 | deephash = rlp.encode(deep_structure) 59 | deephash = base64.urlsafe_b64encode(deephash) 60 | deephash = deephash.decode().rstrip('=') 61 | 62 | # Create deeplink URL 63 | deeplink = self.base_url + '/' + deephash 64 | 65 | # If password check needed, add (`p` URL param) 66 | if password: 67 | password = base64.urlsafe_b64encode(password.encode()) 68 | password = password.decode().rstrip('=') 69 | 70 | deeplink += '?p=' + password 71 | 72 | return deeplink 73 | -------------------------------------------------------------------------------- /mintersdk/sdk/transactions.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Roman Matusevich 3 | """ 4 | import rlp 5 | import hashlib 6 | import copy 7 | from mintersdk import ( 8 | MinterHelper, PREFIX_ADDR, PREFIX_TX, PREFIX_PUBKEY, PREFIX_CHECK 9 | ) 10 | from mintersdk.sdk import ECDSA 11 | from mintersdk.sdk.wallet import MinterWallet 12 | 13 | 14 | class MinterTx(object): 15 | """ 16 | Base transaction class. 17 | Used only for inheritance by real transaction classes. 18 | """ 19 | 20 | # Fee in PIP 21 | PAYLOAD_COMMISSION = 2 22 | 23 | # All gas price multiplied by FEE DEFAULT (PIP) 24 | FEE_DEFAULT_MULTIPLIER = 1000000000000000 25 | 26 | # Type of single signature for the transaction 27 | SIGNATURE_SINGLE_TYPE = 1 28 | 29 | # Type of multi signature for the transaction 30 | SIGNATURE_MULTI_TYPE = 2 31 | 32 | # Main net chain id 33 | MAINNET_CHAIN_ID = 1 34 | 35 | # Test net chain id 36 | TESTNET_CHAIN_ID = 2 37 | 38 | # Each minter transaction has: 39 | # Nonce - int, used for prevent transaction reply. 40 | # Gas Price - big int, used for managing transaction fees. 41 | # Gas Coin - 10 bytes, symbol of a coin to pay fee 42 | # Type - type of transaction (is not needed for base tx class) 43 | # Data - data of transaction (depends on transaction type). 44 | # Payload (arbitrary bytes) - arbitrary user-defined bytes. 45 | # Service Data - reserved field. 46 | # Signature Type - single or multisig transaction. 47 | # Signature Data - digital signature of transaction. 48 | # Don't change this dict directly. You need to copy this dict 49 | # and make needed changes. 50 | _STRUCTURE_DICT = { 51 | 'nonce': None, 52 | 'chain_id': None, 53 | 'gas_price': None, 54 | 'gas_coin': None, 55 | 'type': None, 56 | 'data': None, 57 | 'payload': '', 58 | 'service_data': '', 59 | 'signature_type': None, 60 | 'signature_data': '' 61 | } 62 | 63 | def __init__(self, nonce, gas_coin, payload='', service_data='', 64 | chain_id=1, gas_price=1, **kwargs): 65 | if self.__class__ is MinterTx: 66 | exc_msg = """You can not directly create instance of MinterTx. 67 | Please use one of subclasses ({}) to create needed transaction.""" 68 | raise Exception(exc_msg.format(self.__class__.__subclasses__())) 69 | 70 | # Set every tx attributes 71 | self.nonce = nonce 72 | self.chain_id = chain_id 73 | self.gas_coin = gas_coin.upper() 74 | self.gas_price = gas_price 75 | self.payload = payload 76 | self.service_data = service_data 77 | self.signature_type = None 78 | self.signed_tx = None 79 | 80 | for name, value in kwargs.items(): 81 | setattr(self, name, value) 82 | 83 | self.validate_attrs() 84 | 85 | def validate_attrs(self): 86 | """ Validate init arguments """ 87 | if type(self.nonce) is not int: 88 | raise ValueError(f"'nonce' should be 'int'") 89 | 90 | def generate_tx_rlp(self): 91 | """ 92 | Create structure from instance and prepare rlp structure 93 | Returns: 94 | tx_struct (dict) 95 | """ 96 | # Get structure populated with instance data 97 | tx_struct = self._structure_from_instance() 98 | # Remove signature data, it's not needed before getting Keccak 99 | tx_struct.pop('signature_data') 100 | 101 | # Encode tx data to RLP 102 | tx_struct['data'] = rlp.encode(list(tx_struct['data'].values())) 103 | 104 | return tx_struct 105 | 106 | @staticmethod 107 | def generate_signed_tx(tx_struct): 108 | """ 109 | Generate signed tx hash from it's structure 110 | Args: 111 | tx_struct (dict): populated tx structure dict 112 | Returns: 113 | signed_tx hash (str) 114 | """ 115 | tx_rlp = rlp.encode(list(tx_struct.values())) 116 | return tx_rlp.hex() 117 | 118 | def generate_signature(self, private_key): 119 | """ 120 | Create signature for transaction 121 | Args: 122 | private_key (str): private key to sign with 123 | Returns: 124 | hex_signature (str) 125 | """ 126 | # Get structure populated with instance data and rlp encoded 127 | tx_struct = self.generate_tx_rlp() 128 | 129 | # Create keccak hash 130 | tx_rlp = rlp.encode(list(tx_struct.values())) 131 | keccak = MinterHelper.keccak_hash(tx_rlp) 132 | 133 | # Create signature 134 | signature = ECDSA.sign(keccak, private_key) 135 | signature = rlp.encode(signature).hex() 136 | 137 | return signature 138 | 139 | def sign(self, private_key=None, signature=None, ms_address=None): 140 | """ 141 | Sign transaction. 142 | This method can be called only from instances of inherited 143 | classes. 144 | Args: 145 | private_key (string|list[string]): private key to sign with 146 | signature (string|list[string]): signature to sign with 147 | ms_address (string): Multi signature address to sign tx by 148 | """ 149 | # Check arguments validity 150 | if not private_key and not signature: 151 | raise Exception( 152 | 'Please, provide either `private_key(s)` or `signature(s)`' 153 | ) 154 | if not ms_address and private_key and type(private_key) is not str: 155 | raise Exception( 156 | ''' 157 | Please, provide a single `private_key` or set `ms_address` 158 | argument for multisig tx 159 | ''' 160 | ) 161 | if not ms_address and signature and type(signature) is not str: 162 | raise Exception( 163 | ''' 164 | Please, provide a single `signature` or set `ms_address` 165 | argument for multisig tx 166 | ''' 167 | ) 168 | 169 | # Set tx signature type 170 | self.signature_type = self.SIGNATURE_SINGLE_TYPE 171 | if ms_address: 172 | if type(private_key) is str: 173 | private_key = [private_key] 174 | if type(signature) is str: 175 | signature = [signature] 176 | self.signature_type = self.SIGNATURE_MULTI_TYPE 177 | 178 | # Get populated and rlp encoded tx structure 179 | tx_struct = self.generate_tx_rlp() 180 | 181 | # Signature data 182 | if self.signature_type == self.SIGNATURE_SINGLE_TYPE: 183 | # Only one of private_key or signature can be provided for 184 | # single signature type tx 185 | if private_key and signature: 186 | raise Exception( 187 | ''' 188 | Please, provide one of `private_key` or `signature` for 189 | single signature type tx 190 | ''' 191 | ) 192 | 193 | # Set signature_data 194 | if private_key: 195 | signature_data = self.generate_signature(private_key) 196 | else: 197 | signature_data = signature 198 | signature_data = self.decode_signature(signature_data) 199 | else: 200 | # Add multisig address to signature 201 | signature_data = [ 202 | bytes.fromhex(MinterHelper.prefix_remove(ms_address)), 203 | [] 204 | ] 205 | 206 | # Sign by each private key and add to total signature data 207 | if private_key: 208 | for pk in private_key: 209 | _signature = self.generate_signature(pk) 210 | signature_data[1].append(self.decode_signature(_signature)) 211 | 212 | # Sign by each signature and add to total signature data 213 | if signature: 214 | for _signature in signature: 215 | signature_data[1].append(self.decode_signature(_signature)) 216 | tx_struct['signature_data'] = rlp.encode(signature_data) 217 | 218 | self.signed_tx = self.generate_signed_tx(tx_struct) 219 | 220 | def get_hash(self): 221 | """ 222 | Generate tx hash with prefix 223 | Returns: 224 | string 225 | """ 226 | if not hasattr(self, 'signed_tx') or not self.signed_tx: 227 | raise AttributeError('You need to sign transaction before') 228 | 229 | # Create SHA256 230 | sha = hashlib.sha256() 231 | sha.update(bytes.fromhex(self.signed_tx)) 232 | 233 | # Return first 64 symbols with prefix 234 | return MinterHelper.prefix_add(sha.hexdigest()[:64], PREFIX_TX) 235 | 236 | def _structure_from_instance(self): 237 | """ 238 | Populating structure dict by instance values. 239 | Prepare values for signing process. 240 | """ 241 | 242 | struct = copy.copy(self._STRUCTURE_DICT) 243 | 244 | struct.update({ 245 | 'nonce': self.nonce, 246 | 'chain_id': self.chain_id, 247 | 'gas_price': self.gas_price, 248 | 'gas_coin': MinterHelper.encode_coin_name(self.gas_coin), 249 | 'payload': self.payload, 250 | 'service_data': self.service_data, 251 | 'signature_type': self.signature_type 252 | }) 253 | 254 | return struct 255 | 256 | @classmethod 257 | def _structure_to_kwargs(cls, structure): 258 | """ 259 | Works with already populated structure and prepare **kwargs for 260 | creating new instance of tx. 261 | """ 262 | 263 | structure.update({ 264 | 'gas_coin': MinterHelper.decode_coin_name(structure['gas_coin']) 265 | }) 266 | 267 | return structure 268 | 269 | def get_fee(self): 270 | """ 271 | Get fee of transaction in PIP. 272 | Returns: 273 | int 274 | """ 275 | # Commission for payload and service_data bytes 276 | payload_gas = ( 277 | MinterHelper.bytes_len(self.payload) * self.PAYLOAD_COMMISSION 278 | ) 279 | service_data_gas = ( 280 | MinterHelper.bytes_len(self.service_data) * self.PAYLOAD_COMMISSION 281 | ) 282 | 283 | # Total commission 284 | commission = self.COMMISSION + payload_gas + service_data_gas 285 | commission *= self.FEE_DEFAULT_MULTIPLIER 286 | 287 | return commission 288 | 289 | @classmethod 290 | def from_raw(cls, raw_tx): 291 | """ 292 | Generate tx object from raw tx 293 | Args: 294 | raw_tx (string) 295 | Returns: 296 | MinterTx child instance 297 | """ 298 | 299 | tx = rlp.decode(bytes.fromhex(raw_tx)) 300 | 301 | # Try to decode payload 302 | try: 303 | payload = tx[6].decode() 304 | except UnicodeDecodeError: 305 | payload = tx[6] 306 | 307 | # Try to decode service data 308 | try: 309 | service_data = tx[7].decode() 310 | except UnicodeDecodeError: 311 | service_data = tx[7] 312 | 313 | # Populate structure dict with decoded tx data 314 | struct = copy.copy(cls._STRUCTURE_DICT) 315 | struct.update({ 316 | 'nonce': int.from_bytes(tx[0], 'big'), 317 | 'chain_id': int.from_bytes(tx[1], 'big'), 318 | 'gas_price': int.from_bytes(tx[2], 'big'), 319 | 'gas_coin': tx[3].decode(), 320 | 'type': int.from_bytes(tx[4], 'big'), 321 | 'payload': payload, 322 | 'service_data': service_data, 323 | 'signature_type': int.from_bytes(tx[8], 'big') 324 | }) 325 | 326 | # Get signature data 327 | signature_data = rlp.decode(tx[9]) 328 | if struct['signature_type'] == cls.SIGNATURE_SINGLE_TYPE: 329 | signature_data = { 330 | 'v': int.from_bytes(signature_data[0], 'big'), 331 | 'r': signature_data[1].hex(), 332 | 's': signature_data[2].hex() 333 | } 334 | else: 335 | # Decode signatures 336 | signatures = [] 337 | for signature in signature_data[1]: 338 | signatures.append({ 339 | 'v': int.from_bytes(signature[0], 'big'), 340 | 'r': signature[1].hex(), 341 | 's': signature[2].hex() 342 | }) 343 | 344 | # Create decoded signature data 345 | signature_data = { 346 | 'from_mx': MinterHelper.prefix_add( 347 | signature_data[0].hex(), PREFIX_ADDR 348 | ), 349 | 'signatures': signatures 350 | } 351 | struct['signature_data'] = signature_data 352 | 353 | # Find out which of tx instance need to create depending on it's type 354 | data = rlp.decode(tx[5]) 355 | if struct['type'] == MinterDelegateTx.TYPE: 356 | _class = MinterDelegateTx 357 | elif struct['type'] == MinterSendCoinTx.TYPE: 358 | _class = MinterSendCoinTx 359 | elif struct['type'] == MinterBuyCoinTx.TYPE: 360 | _class = MinterBuyCoinTx 361 | elif struct['type'] == MinterCreateCoinTx.TYPE: 362 | _class = MinterCreateCoinTx 363 | elif struct['type'] == MinterDeclareCandidacyTx.TYPE: 364 | _class = MinterDeclareCandidacyTx 365 | elif struct['type'] == MinterRedeemCheckTx.TYPE: 366 | _class = MinterRedeemCheckTx 367 | elif struct['type'] == MinterSellAllCoinTx.TYPE: 368 | _class = MinterSellAllCoinTx 369 | elif struct['type'] == MinterSellCoinTx.TYPE: 370 | _class = MinterSellCoinTx 371 | elif struct['type'] == MinterSetCandidateOffTx.TYPE: 372 | _class = MinterSetCandidateOffTx 373 | elif struct['type'] == MinterSetCandidateOnTx.TYPE: 374 | _class = MinterSetCandidateOnTx 375 | elif struct['type'] == MinterUnbondTx.TYPE: 376 | _class = MinterUnbondTx 377 | elif struct['type'] == MinterEditCandidateTx.TYPE: 378 | _class = MinterEditCandidateTx 379 | elif struct['type'] == MinterMultiSendCoinTx.TYPE: 380 | _class = MinterMultiSendCoinTx 381 | elif struct['type'] == MinterCreateMultisigTx.TYPE: 382 | _class = MinterCreateMultisigTx 383 | else: 384 | raise Exception('Undefined tx type.') 385 | 386 | # Set tx data 387 | struct['data'] = _class._data_from_raw(data) 388 | 389 | # Set sender address and raw tx to minter dict 390 | # ONLY AFTER tx data was set 391 | struct.update({ 392 | 'from_mx': cls.get_sender_address(tx=copy.copy(struct)), 393 | 'signed_tx': raw_tx 394 | }) 395 | 396 | # Prepare **kwargs for creating _class instance. 397 | # Pass copy of the struct. 398 | kwargs = _class._structure_to_kwargs(copy.copy(struct)) 399 | 400 | return _class(**kwargs) 401 | 402 | @classmethod 403 | def get_sender_address(cls, tx): 404 | """ 405 | Get sender address from tx. 406 | Recover public key from tx and then get address from public key, 407 | if tx has single signature type, or get decoded sender address, 408 | if tx has multi signature type. 409 | Args: 410 | tx (dict): transaction dict 411 | Returns: 412 | Minter address (string) 413 | """ 414 | 415 | # Remember signature data and remove it from tx 416 | signature_data = tx.pop('signature_data') 417 | 418 | # If there is sender address in signature data (multi signature tx), 419 | # return it 420 | if signature_data.get('from_mx'): 421 | return signature_data['from_mx'] 422 | 423 | # Otherwise (single signature tx), recover public key and get 424 | # address from public key 425 | # Unhexlify hexdigit dict values to bytes 426 | tx = MinterHelper.hex2bin_recursive(tx) 427 | 428 | # Encode tx data to RLP 429 | tx['data'] = rlp.encode(list(tx['data'].values())) 430 | 431 | # Message 432 | tx_rlp = rlp.encode(list(tx.values())) 433 | _keccak = MinterHelper.keccak_hash(tx_rlp) 434 | 435 | # Recover public key 436 | public_key = MinterHelper.prefix_add( 437 | ECDSA.recover(_keccak, tuple(signature_data.values())), 438 | PREFIX_PUBKEY 439 | ) 440 | 441 | return MinterWallet.get_address_from_public_key(public_key) 442 | 443 | @classmethod 444 | def add_signature(cls, signed_tx, private_key): 445 | """ 446 | Add signature to already signed tx. Method is available only for multisig txs 447 | Args: 448 | signed_tx (str): signed tx 449 | private_key (str): private key 450 | Returns: 451 | tx object 452 | """ 453 | 454 | # Create tx instance from raw tx 455 | tx = cls.from_raw(signed_tx) 456 | 457 | # Check tx to be multi signature type 458 | if tx.signature_type != cls.SIGNATURE_MULTI_TYPE: 459 | raise Exception( 460 | 'Signature can be added only to tx with multi signature type' 461 | ) 462 | 463 | # Convert signature data from verbose dict to needed list of 464 | # signatures 465 | signature_data = [ 466 | bytes.fromhex( 467 | MinterHelper.prefix_remove(tx.signature_data['from_mx']) 468 | ), 469 | [] 470 | ] 471 | for item in tx.signature_data['signatures']: 472 | # Create raw signature (values as integers) 473 | raw = [ 474 | item['v'], 475 | int(item['r'], 16), 476 | int(item['s'], 16) 477 | ] 478 | # Append raw signature to total signature data 479 | signature_data[1].append(raw) 480 | 481 | # Get tx populated structure and keccak hash from this structure 482 | tx_struct = tx.generate_tx_rlp() 483 | 484 | # Create new signature and update signatures list and signatures 485 | # attribute of tx 486 | signature = cls.decode_signature( 487 | signature=tx.generate_signature(private_key) 488 | ) 489 | signature_data[1].append(signature) 490 | tx.signature_data['signatures'].append({ 491 | 'v': signature[0], 492 | 'r': format(signature[1], 'x'), 493 | 's': format(signature[2], 'x') 494 | }) 495 | 496 | # Update resulting struct signature data 497 | tx_struct['signature_data'] = rlp.encode(signature_data) 498 | 499 | # Generate new signed tx and update tx object attribute 500 | tx.signed_tx = cls.generate_signed_tx(tx_struct) 501 | 502 | return tx 503 | 504 | @classmethod 505 | def decode_signature(cls, signature): 506 | """ 507 | Decode hexed signature to raw signature (list of integers) 508 | Args: 509 | signature (str): hexed signature 510 | Returns: 511 | signature (list[int]) 512 | """ 513 | signature = bytes.fromhex(signature) 514 | signature = rlp.decode(signature) 515 | signature = [int.from_bytes(item, 'big') for item in signature] 516 | 517 | return signature 518 | 519 | @classmethod 520 | def _data_from_raw(cls, raw_data): 521 | """ 522 | Decoding tx data to tx attributes. 523 | Decoding depends on tx type, so this method 524 | must be implemented in each child class. 525 | """ 526 | raise NotImplementedError 527 | 528 | 529 | class MinterBuyCoinTx(MinterTx): 530 | """ Buy coin transaction """ 531 | 532 | # Type of transaction 533 | TYPE = 4 534 | 535 | # Fee units 536 | COMMISSION = 100 537 | 538 | def __init__(self, coin_to_buy, value_to_buy, coin_to_sell, 539 | max_value_to_sell, **kwargs): 540 | """ 541 | Args: 542 | coin_to_buy (str): coin name to buy 543 | value_to_buy (float|int): how much coin to buy (BIP) 544 | coin_to_sell (str): coin name to sell 545 | max_value_to_sell (float|int): max amount to sell (BIP) 546 | """ 547 | 548 | super().__init__(**kwargs) 549 | 550 | self.coin_to_buy = coin_to_buy.upper() 551 | self.value_to_buy = value_to_buy 552 | self.coin_to_sell = coin_to_sell.upper() 553 | self.max_value_to_sell = max_value_to_sell 554 | 555 | def _structure_from_instance(self): 556 | """ Override parent method. """ 557 | 558 | struct = super()._structure_from_instance() 559 | 560 | struct.update({ 561 | 'type': self.TYPE, 562 | 'data': { 563 | 'coin_to_buy': MinterHelper.encode_coin_name(self.coin_to_buy), 564 | 'value_to_buy': MinterHelper.to_pip(self.value_to_buy), 565 | 'coin_to_sell': MinterHelper.encode_coin_name(self.coin_to_sell), 566 | 'max_value_to_sell': MinterHelper.to_pip(self.max_value_to_sell) 567 | } 568 | }) 569 | 570 | return struct 571 | 572 | @classmethod 573 | def _structure_to_kwargs(cls, structure): 574 | """ Prepare decoded structure data to instance kwargs. """ 575 | 576 | kwargs = super()._structure_to_kwargs(structure) 577 | 578 | # Convert data values to verbose. 579 | # Data will be passed as additional kwarg 580 | kwargs['data'].update({ 581 | 'coin_to_buy': MinterHelper.decode_coin_name( 582 | kwargs['data']['coin_to_buy'] 583 | ), 584 | 'value_to_buy': MinterHelper.to_bip( 585 | int.from_bytes(kwargs['data']['value_to_buy'], 'big') 586 | ), 587 | 'coin_to_sell': MinterHelper.decode_coin_name( 588 | kwargs['data']['coin_to_sell'] 589 | ), 590 | 'max_value_to_sell': MinterHelper.to_bip( 591 | int.from_bytes(kwargs['data']['max_value_to_sell'], 'big') 592 | ) 593 | }) 594 | 595 | # Populate data key values as kwargs 596 | kwargs.update(kwargs['data']) 597 | 598 | return kwargs 599 | 600 | @classmethod 601 | def _data_from_raw(cls, raw_data): 602 | """ Parent method implementation """ 603 | return { 604 | 'coin_to_buy': raw_data[0], 605 | 'value_to_buy': raw_data[1], 606 | 'coin_to_sell': raw_data[2], 607 | 'max_value_to_sell': raw_data[3] 608 | } 609 | 610 | 611 | class MinterCreateCoinTx(MinterTx): 612 | """ Create coin transaction """ 613 | 614 | # Type of transaction 615 | TYPE = 5 616 | 617 | # Fee units 618 | COMMISSION = 1000 619 | 620 | def __init__(self, name, symbol, initial_amount, initial_reserve, crr, 621 | max_supply, **kwargs): 622 | """ 623 | Args: 624 | name (str): coin name 625 | symbol (str): coin symbol 626 | initial_amount (float|int): amount in BIP 627 | initial_reserve (float|int): reserve in BIP 628 | crr (int) 629 | """ 630 | 631 | super().__init__(**kwargs) 632 | 633 | self.name = name 634 | self.symbol = symbol.upper() 635 | self.initial_amount = initial_amount 636 | self.initial_reserve = initial_reserve 637 | self.crr = crr 638 | self.max_supply = max_supply 639 | 640 | def _structure_from_instance(self): 641 | """ Override parent method. """ 642 | 643 | struct = super()._structure_from_instance() 644 | 645 | struct.update({ 646 | 'type': self.TYPE, 647 | 'data': { 648 | 'name': self.name, 649 | 'symbol': MinterHelper.encode_coin_name(self.symbol), 650 | 'initial_amount': MinterHelper.to_pip(self.initial_amount), 651 | 'initial_reserve': MinterHelper.to_pip(self.initial_reserve), 652 | 'crr': '' if self.crr == 0 else self.crr, 653 | 'max_supply': MinterHelper.to_pip(self.max_supply) 654 | } 655 | }) 656 | 657 | return struct 658 | 659 | @classmethod 660 | def _structure_to_kwargs(cls, structure): 661 | """ Prepare decoded structure data to instance kwargs. """ 662 | 663 | kwargs = super()._structure_to_kwargs(structure) 664 | 665 | # Convert data values to verbose. 666 | # Data will be passed as additional kwarg 667 | kwargs['data'].update({ 668 | 'name': kwargs['data']['name'].decode(), 669 | 'symbol': MinterHelper.decode_coin_name(kwargs['data']['symbol']), 670 | 'initial_amount': MinterHelper.to_bip( 671 | int.from_bytes(kwargs['data']['initial_amount'], 'big') 672 | ), 673 | 'initial_reserve': MinterHelper.to_bip( 674 | int.from_bytes(kwargs['data']['initial_reserve'], 'big') 675 | ), 676 | 'crr': int.from_bytes(kwargs['data']['crr'], 'big'), 677 | 'max_supply': MinterHelper.to_bip( 678 | int.from_bytes(kwargs['data']['max_supply'], 'big') 679 | ) 680 | }) 681 | 682 | # Populate data key values as kwargs 683 | kwargs.update(kwargs['data']) 684 | 685 | return kwargs 686 | 687 | @classmethod 688 | def _data_from_raw(cls, raw_data): 689 | """ Parent method implementation """ 690 | return { 691 | 'name': raw_data[0], 692 | 'symbol': raw_data[1], 693 | 'initial_amount': raw_data[2], 694 | 'initial_reserve': raw_data[3], 695 | 'crr': raw_data[4], 696 | 'max_supply': raw_data[5] 697 | } 698 | 699 | 700 | class MinterDeclareCandidacyTx(MinterTx): 701 | """ Declare candidacy transaction """ 702 | 703 | # Type of transaction 704 | TYPE = 6 705 | 706 | # Fee units 707 | COMMISSION = 10000 708 | 709 | def __init__(self, address, pub_key, commission, coin, stake, **kwargs): 710 | """ 711 | Args: 712 | address (str): candidate address 713 | pub_key (str): candidate public key 714 | commission (int): candidate commission 715 | coin (str): coin name 716 | stake (float|int): stake in BIP 717 | """ 718 | 719 | super().__init__(**kwargs) 720 | 721 | self.address = address 722 | self.pub_key = pub_key 723 | self.commission = '' if commission == 0 else commission 724 | self.coin = coin.upper() 725 | self.stake = stake 726 | 727 | def _structure_from_instance(self): 728 | """ Override parent method. """ 729 | 730 | struct = super()._structure_from_instance() 731 | 732 | struct.update({ 733 | 'type': self.TYPE, 734 | 'data': { 735 | 'address': bytes.fromhex( 736 | MinterHelper.prefix_remove(self.address) 737 | ), 738 | 'pub_key': bytes.fromhex( 739 | MinterHelper.prefix_remove(self.pub_key) 740 | ), 741 | 'commission': '' if self.commission == 0 else self.commission, 742 | 'coin': MinterHelper.encode_coin_name(self.coin), 743 | 'stake': MinterHelper.to_pip(self.stake) 744 | } 745 | }) 746 | 747 | return struct 748 | 749 | @classmethod 750 | def _structure_to_kwargs(cls, structure): 751 | """ Prepare decoded structure data to instance kwargs. """ 752 | 753 | kwargs = super()._structure_to_kwargs(structure) 754 | 755 | # Convert data values to verbose. 756 | # Data will be passed as additional kwarg 757 | kwargs['data'].update({ 758 | 'address': MinterHelper.prefix_add( 759 | kwargs['data']['address'].hex(), PREFIX_ADDR 760 | ), 761 | 'pub_key': MinterHelper.prefix_add( 762 | kwargs['data']['pub_key'].hex(), PREFIX_PUBKEY 763 | ), 764 | 'commission': int.from_bytes(kwargs['data']['commission'], 'big'), 765 | 'coin': MinterHelper.decode_coin_name(kwargs['data']['coin']), 766 | 'stake': MinterHelper.to_bip( 767 | int.from_bytes(kwargs['data']['stake'], 'big') 768 | ) 769 | }) 770 | 771 | # Populate data key values as kwargs 772 | kwargs.update(kwargs['data']) 773 | 774 | return kwargs 775 | 776 | @classmethod 777 | def _data_from_raw(cls, raw_data): 778 | """ Parent method implementation """ 779 | return { 780 | 'address': raw_data[0], 781 | 'pub_key': raw_data[1], 782 | 'commission': raw_data[2], 783 | 'coin': raw_data[3], 784 | 'stake': raw_data[4] 785 | } 786 | 787 | 788 | class MinterDelegateTx(MinterTx): 789 | """ Delegate transaction """ 790 | 791 | # Type of transaction 792 | TYPE = 7 793 | 794 | # Fee units 795 | COMMISSION = 200 796 | 797 | def __init__(self, pub_key, coin, stake, **kwargs): 798 | super().__init__(**kwargs) 799 | 800 | self.pub_key = pub_key 801 | self.coin = coin.upper() 802 | self.stake = stake 803 | 804 | def _structure_from_instance(self): 805 | """ Override parent method to add tx special data. """ 806 | 807 | struct = super()._structure_from_instance() 808 | 809 | struct.update({ 810 | 'type': self.TYPE, 811 | 'data': { 812 | 'pub_key': bytes.fromhex( 813 | MinterHelper.prefix_remove(self.pub_key) 814 | ), 815 | 'coin': MinterHelper.encode_coin_name(self.coin), 816 | 'stake': MinterHelper.to_pip(self.stake) 817 | } 818 | }) 819 | 820 | return struct 821 | 822 | @classmethod 823 | def _structure_to_kwargs(cls, structure): 824 | """ Prepare decoded structure data to instance kwargs. """ 825 | kwargs = super()._structure_to_kwargs(structure) 826 | 827 | # Convert data values to verbose. 828 | # Data will be passed as additional kwarg 829 | kwargs['data'].update({ 830 | 'pub_key': MinterHelper.prefix_add( 831 | kwargs['data']['pub_key'].hex(), PREFIX_PUBKEY 832 | ), 833 | 'coin': MinterHelper.decode_coin_name(kwargs['data']['coin']), 834 | 'stake': MinterHelper.to_bip( 835 | int.from_bytes(kwargs['data']['stake'], 'big') 836 | ) 837 | }) 838 | 839 | # Populate data key values as kwargs 840 | kwargs.update(kwargs['data']) 841 | 842 | return kwargs 843 | 844 | @classmethod 845 | def _data_from_raw(cls, raw_data): 846 | """ Parent method implementation """ 847 | return { 848 | 'pub_key': raw_data[0], 849 | 'coin': raw_data[1], 850 | 'stake': raw_data[2] 851 | } 852 | 853 | 854 | class MinterRedeemCheckTx(MinterTx): 855 | """ Redeem check transaction """ 856 | 857 | # Type of transaction 858 | TYPE = 9 859 | 860 | # Fee units 861 | COMMISSION = 10 862 | 863 | def __init__(self, check, proof, **kwargs): 864 | """ 865 | Args: 866 | check (str) 867 | proof (str) 868 | """ 869 | 870 | super().__init__(**kwargs) 871 | 872 | self.check = check 873 | self.proof = proof 874 | 875 | def _structure_from_instance(self): 876 | """ Override parent method to add tx special data. """ 877 | 878 | struct = super()._structure_from_instance() 879 | 880 | struct.update({ 881 | 'type': self.TYPE, 882 | 'data': { 883 | 'check': bytes.fromhex(MinterHelper.prefix_remove(self.check)), 884 | 'proof': bytes.fromhex(self.proof) 885 | } 886 | }) 887 | 888 | return struct 889 | 890 | @classmethod 891 | def _structure_to_kwargs(cls, structure): 892 | """ Prepare decoded structure data to instance kwargs. """ 893 | 894 | kwargs = super()._structure_to_kwargs(structure) 895 | 896 | # Convert data values to verbose. 897 | # Data will be passed as additional kwarg 898 | kwargs['data'].update({ 899 | 'check': MinterHelper.prefix_add( 900 | kwargs['data']['check'].hex(), PREFIX_CHECK 901 | ), 902 | 'proof': kwargs['data']['proof'].hex() 903 | }) 904 | 905 | # Populate data key values as kwargs 906 | kwargs.update(kwargs['data']) 907 | 908 | return kwargs 909 | 910 | @classmethod 911 | def _data_from_raw(cls, raw_data): 912 | """ Parent method implementation """ 913 | return { 914 | 'check': raw_data[0], 915 | 'proof': raw_data[1] 916 | } 917 | 918 | 919 | class MinterSellAllCoinTx(MinterTx): 920 | """ Sell all coin transaction """ 921 | 922 | # Type of transaction 923 | TYPE = 3 924 | 925 | # Fee units 926 | COMMISSION = 100 927 | 928 | def __init__(self, coin_to_sell, coin_to_buy, min_value_to_buy, **kwargs): 929 | """ 930 | Args: 931 | coin_to_sell (str) 932 | coin_to_buy (str) 933 | min_value_to_buy (float|int): BIP 934 | """ 935 | 936 | super().__init__(**kwargs) 937 | 938 | self.coin_to_sell = coin_to_sell.upper() 939 | self.coin_to_buy = coin_to_buy.upper() 940 | self.min_value_to_buy = min_value_to_buy 941 | 942 | def _structure_from_instance(self): 943 | """ Override parent method to add tx special data. """ 944 | 945 | struct = super()._structure_from_instance() 946 | 947 | struct.update({ 948 | 'type': self.TYPE, 949 | 'data': { 950 | 'coin_to_sell': MinterHelper.encode_coin_name(self.coin_to_sell), 951 | 'coin_to_buy': MinterHelper.encode_coin_name(self.coin_to_buy), 952 | 'min_value_to_buy': MinterHelper.to_pip(self.min_value_to_buy) 953 | } 954 | }) 955 | 956 | return struct 957 | 958 | @classmethod 959 | def _structure_to_kwargs(cls, structure): 960 | """ Prepare decoded structure data to instance kwargs. """ 961 | 962 | kwargs = super()._structure_to_kwargs(structure) 963 | 964 | # Convert data values to verbose. 965 | # Data will be passed as additional kwarg 966 | kwargs['data'].update({ 967 | 'coin_to_sell': MinterHelper.decode_coin_name( 968 | kwargs['data']['coin_to_sell'] 969 | ), 970 | 'coin_to_buy': MinterHelper.decode_coin_name( 971 | kwargs['data']['coin_to_buy'] 972 | ), 973 | 'min_value_to_buy': MinterHelper.to_bip( 974 | int.from_bytes(kwargs['data']['min_value_to_buy'], 'big') 975 | ) 976 | }) 977 | 978 | # Populate data key values as kwargs 979 | kwargs.update(kwargs['data']) 980 | 981 | return kwargs 982 | 983 | @classmethod 984 | def _data_from_raw(cls, raw_data): 985 | """ Parent method implementation """ 986 | return { 987 | 'coin_to_sell': raw_data[0], 988 | 'coin_to_buy': raw_data[1], 989 | 'min_value_to_buy': raw_data[2] 990 | } 991 | 992 | 993 | class MinterSellCoinTx(MinterTx): 994 | """ Sell coin transaction """ 995 | 996 | # Type of transaction 997 | TYPE = 2 998 | 999 | # Fee units 1000 | COMMISSION = 100 1001 | 1002 | def __init__(self, coin_to_sell, value_to_sell, coin_to_buy, 1003 | min_value_to_buy, **kwargs): 1004 | """ 1005 | Args: 1006 | coin_to_sell (str) 1007 | value_to_sell (float|int): BIP 1008 | coin_to_buy (str) 1009 | min_value_to_buy (float|int): BIP 1010 | """ 1011 | 1012 | super().__init__(**kwargs) 1013 | 1014 | self.coin_to_sell = coin_to_sell.upper() 1015 | self.value_to_sell = value_to_sell 1016 | self.coin_to_buy = coin_to_buy.upper() 1017 | self.min_value_to_buy = min_value_to_buy 1018 | 1019 | def _structure_from_instance(self): 1020 | """ Override parent method to add tx special data. """ 1021 | 1022 | struct = super()._structure_from_instance() 1023 | 1024 | struct.update({ 1025 | 'type': self.TYPE, 1026 | 'data': { 1027 | 'coin_to_sell': MinterHelper.encode_coin_name(self.coin_to_sell), 1028 | 'value_to_sell': MinterHelper.to_pip(self.value_to_sell), 1029 | 'coin_to_buy': MinterHelper.encode_coin_name(self.coin_to_buy), 1030 | 'min_value_to_buy': MinterHelper.to_pip(self.min_value_to_buy) 1031 | } 1032 | }) 1033 | 1034 | return struct 1035 | 1036 | @classmethod 1037 | def _structure_to_kwargs(cls, structure): 1038 | """ Prepare decoded structure data to instance kwargs. """ 1039 | 1040 | kwargs = super()._structure_to_kwargs(structure) 1041 | 1042 | # Convert data values to verbose. 1043 | # Data will be passed as additional kwarg 1044 | kwargs['data'].update({ 1045 | 'coin_to_sell': MinterHelper.decode_coin_name( 1046 | kwargs['data']['coin_to_sell'] 1047 | ), 1048 | 'value_to_sell': MinterHelper.to_bip( 1049 | int.from_bytes(kwargs['data']['value_to_sell'], 'big') 1050 | ), 1051 | 'coin_to_buy': MinterHelper.decode_coin_name( 1052 | kwargs['data']['coin_to_buy'] 1053 | ), 1054 | 'min_value_to_buy': MinterHelper.to_bip( 1055 | int.from_bytes(kwargs['data']['min_value_to_buy'], 'big') 1056 | ) 1057 | }) 1058 | 1059 | # Populate data key values as kwargs 1060 | kwargs.update(kwargs['data']) 1061 | 1062 | return kwargs 1063 | 1064 | @classmethod 1065 | def _data_from_raw(cls, raw_data): 1066 | """ Parent method implementation """ 1067 | return { 1068 | 'coin_to_sell': raw_data[0], 1069 | 'value_to_sell': raw_data[1], 1070 | 'coin_to_buy': raw_data[2], 1071 | 'min_value_to_buy': raw_data[3] 1072 | } 1073 | 1074 | 1075 | class MinterSendCoinTx(MinterTx): 1076 | """ Send coin transaction """ 1077 | 1078 | # Type of transaction 1079 | TYPE = 1 1080 | 1081 | # Fee units 1082 | COMMISSION = 10 1083 | 1084 | def __init__(self, coin, to, value, **kwargs): 1085 | super().__init__(**kwargs) 1086 | 1087 | self.coin = coin.upper() 1088 | self.to = to 1089 | self.value = value 1090 | 1091 | def _structure_from_instance(self): 1092 | """ Override parent method to add tx special data. """ 1093 | 1094 | struct = super()._structure_from_instance() 1095 | 1096 | struct.update({ 1097 | 'type': self.TYPE, 1098 | 'data': { 1099 | 'coin': MinterHelper.encode_coin_name(self.coin), 1100 | 'to': bytes.fromhex(MinterHelper.prefix_remove(self.to)), 1101 | 'value': MinterHelper.to_pip(self.value) 1102 | } 1103 | }) 1104 | 1105 | return struct 1106 | 1107 | @classmethod 1108 | def _structure_to_kwargs(cls, structure): 1109 | """ Prepare decoded structure data to instance kwargs. """ 1110 | 1111 | kwargs = super()._structure_to_kwargs(structure) 1112 | 1113 | # Convert data values to verbose. 1114 | # Data will be passed as additional kwarg 1115 | kwargs['data'].update({ 1116 | 'coin': MinterHelper.decode_coin_name(kwargs['data']['coin']), 1117 | 'to': MinterHelper.prefix_add( 1118 | kwargs['data']['to'].hex(), PREFIX_ADDR 1119 | ), 1120 | 'value': MinterHelper.to_bip( 1121 | int.from_bytes(kwargs['data']['value'], 'big') 1122 | ) 1123 | }) 1124 | 1125 | # Populate data key values as kwargs 1126 | kwargs.update(kwargs['data']) 1127 | 1128 | return kwargs 1129 | 1130 | @classmethod 1131 | def _data_from_raw(cls, raw_data): 1132 | """ Parent method implementation """ 1133 | return { 1134 | 'coin': raw_data[0], 1135 | 'to': raw_data[1], 1136 | 'value': raw_data[2] 1137 | } 1138 | 1139 | 1140 | class MinterMultiSendCoinTx(MinterTx): 1141 | """ Multi send transaction """ 1142 | 1143 | # Type of transaction 1144 | TYPE = 13 1145 | 1146 | # Fee units 1147 | COMMISSION = 10 1148 | COMMISSION_PER_RECIPIENT = 5 1149 | 1150 | def __init__(self, txs, **kwargs): 1151 | """ 1152 | Args: 1153 | txs (list[dict{coin, to, value}]): list of send coin data 1154 | """ 1155 | super().__init__(**kwargs) 1156 | 1157 | self.txs = txs 1158 | 1159 | def get_fee(self): 1160 | """ 1161 | Override parent method to calc 1162 | multisend-specific fee: (n_txs - 1) * 5 units 1163 | """ 1164 | base_fee = super().get_fee() 1165 | recipients_fee = ( 1166 | (len(self.txs)-1) * self.COMMISSION_PER_RECIPIENT * 1167 | self.FEE_DEFAULT_MULTIPLIER 1168 | ) 1169 | 1170 | return base_fee + recipients_fee 1171 | 1172 | def _structure_from_instance(self): 1173 | """ Override parent method to add tx special data. """ 1174 | 1175 | struct = super()._structure_from_instance() 1176 | 1177 | struct.update({ 1178 | 'type': self.TYPE, 1179 | 'data': { 1180 | 'txs': [] 1181 | } 1182 | }) 1183 | 1184 | # Populate multi data from each single tx. 1185 | for item in self.txs: 1186 | struct['data']['txs'].append([ 1187 | MinterHelper.encode_coin_name(item['coin'].upper()), 1188 | bytes.fromhex(MinterHelper.prefix_remove(item['to'])), 1189 | MinterHelper.to_pip(item['value']) 1190 | ]) 1191 | 1192 | return struct 1193 | 1194 | @classmethod 1195 | def _structure_to_kwargs(cls, structure): 1196 | """ Prepare decoded structure data to instance kwargs. """ 1197 | 1198 | kwargs = super()._structure_to_kwargs(structure) 1199 | 1200 | # Convert data values to verbose. 1201 | # Data will be passed as additional kwarg 1202 | for index, item in enumerate(kwargs['data']['txs']): 1203 | kwargs['data']['txs'][index] = { 1204 | 'coin': MinterHelper.decode_coin_name(item[0]), 1205 | 'to': MinterHelper.prefix_add(item[1].hex(), PREFIX_ADDR), 1206 | 'value': MinterHelper.to_bip(int.from_bytes(item[2], 'big')) 1207 | } 1208 | 1209 | # Populate data key values as kwargs 1210 | kwargs.update(kwargs['data']) 1211 | 1212 | return kwargs 1213 | 1214 | @classmethod 1215 | def _data_from_raw(cls, raw_data): 1216 | """ Parent method implementation """ 1217 | data = {'txs': []} 1218 | for item in raw_data[0]: 1219 | data['txs'].append([item[0], item[1], item[2]]) 1220 | 1221 | return data 1222 | 1223 | 1224 | class MinterSetCandidateOffTx(MinterTx): 1225 | """ Set candidate OFF transaction """ 1226 | 1227 | # Type of transaction 1228 | TYPE = 11 1229 | 1230 | # Fee units 1231 | COMMISSION = 100 1232 | 1233 | def __init__(self, pub_key, **kwargs): 1234 | """ 1235 | Args: 1236 | pub_key (str) 1237 | """ 1238 | 1239 | super().__init__(**kwargs) 1240 | 1241 | self.pub_key = pub_key 1242 | 1243 | def _structure_from_instance(self): 1244 | """ Override parent method to add tx special data. """ 1245 | 1246 | struct = super()._structure_from_instance() 1247 | 1248 | struct.update({ 1249 | 'type': self.TYPE, 1250 | 'data': { 1251 | 'pub_key': bytes.fromhex( 1252 | MinterHelper.prefix_remove(self.pub_key) 1253 | ) 1254 | } 1255 | }) 1256 | 1257 | return struct 1258 | 1259 | @classmethod 1260 | def _structure_to_kwargs(cls, structure): 1261 | """ Prepare decoded structure data to instance kwargs. """ 1262 | 1263 | kwargs = super()._structure_to_kwargs(structure) 1264 | 1265 | # Convert data values to verbose. 1266 | # Data will be passed as additional kwarg 1267 | kwargs['data'].update({ 1268 | 'pub_key': MinterHelper.prefix_add( 1269 | kwargs['data']['pub_key'].hex(), PREFIX_PUBKEY 1270 | ) 1271 | }) 1272 | 1273 | # Populate data key values as kwargs 1274 | kwargs.update(kwargs['data']) 1275 | 1276 | return kwargs 1277 | 1278 | @classmethod 1279 | def _data_from_raw(cls, raw_data): 1280 | """ Parent method implementation """ 1281 | return { 1282 | 'pub_key': raw_data[0] 1283 | } 1284 | 1285 | 1286 | class MinterSetCandidateOnTx(MinterTx): 1287 | """ Set candidate ON transaction """ 1288 | 1289 | # Type of transaction 1290 | TYPE = 10 1291 | 1292 | # Fee units 1293 | COMMISSION = 100 1294 | 1295 | def __init__(self, pub_key, **kwargs): 1296 | """ 1297 | Args: 1298 | pub_key (str) 1299 | """ 1300 | 1301 | super().__init__(**kwargs) 1302 | 1303 | self.pub_key = pub_key 1304 | 1305 | def _structure_from_instance(self): 1306 | """ Override parent method to add tx special data. """ 1307 | 1308 | struct = super()._structure_from_instance() 1309 | 1310 | struct.update({ 1311 | 'type': self.TYPE, 1312 | 'data': { 1313 | 'pub_key': bytes.fromhex( 1314 | MinterHelper.prefix_remove(self.pub_key) 1315 | ) 1316 | } 1317 | }) 1318 | 1319 | return struct 1320 | 1321 | @classmethod 1322 | def _structure_to_kwargs(cls, structure): 1323 | """ Prepare decoded structure data to instance kwargs. """ 1324 | 1325 | kwargs = super()._structure_to_kwargs(structure) 1326 | 1327 | # Convert data values to verbose. 1328 | # Data will be passed as additional kwarg 1329 | kwargs['data'].update({ 1330 | 'pub_key': MinterHelper.prefix_add( 1331 | kwargs['data']['pub_key'].hex(), PREFIX_PUBKEY 1332 | ) 1333 | }) 1334 | 1335 | # Populate data key values as kwargs 1336 | kwargs.update(kwargs['data']) 1337 | 1338 | return kwargs 1339 | 1340 | @classmethod 1341 | def _data_from_raw(cls, raw_data): 1342 | """ Parent method implementation """ 1343 | return { 1344 | 'pub_key': raw_data[0] 1345 | } 1346 | 1347 | 1348 | class MinterUnbondTx(MinterTx): 1349 | """ Unbond transaction """ 1350 | 1351 | # Type of transaction 1352 | TYPE = 8 1353 | 1354 | # Fee units 1355 | COMMISSION = 100 1356 | 1357 | def __init__(self, pub_key, coin, value, **kwargs): 1358 | """ 1359 | Args: 1360 | pub_key (str) 1361 | coin (str) 1362 | value (float|int): BIP 1363 | """ 1364 | 1365 | super().__init__(**kwargs) 1366 | 1367 | self.pub_key = pub_key 1368 | self.coin = coin.upper() 1369 | self.value = value 1370 | 1371 | def _structure_from_instance(self): 1372 | """ Override parent method to add tx special data. """ 1373 | 1374 | struct = super()._structure_from_instance() 1375 | 1376 | struct.update({ 1377 | 'type': self.TYPE, 1378 | 'data': { 1379 | 'pub_key': bytes.fromhex( 1380 | MinterHelper.prefix_remove(self.pub_key) 1381 | ), 1382 | 'coin': MinterHelper.encode_coin_name(self.coin), 1383 | 'value': MinterHelper.to_pip(self.value) 1384 | } 1385 | }) 1386 | 1387 | return struct 1388 | 1389 | @classmethod 1390 | def _structure_to_kwargs(cls, structure): 1391 | """ Prepare decoded structure data to instance kwargs. """ 1392 | 1393 | kwargs = super()._structure_to_kwargs(structure) 1394 | 1395 | # Convert data values to verbose. 1396 | # Data will be passed as additional kwarg 1397 | kwargs['data'].update({ 1398 | 'pub_key': MinterHelper.prefix_add( 1399 | kwargs['data']['pub_key'].hex(), PREFIX_PUBKEY 1400 | ), 1401 | 'coin': MinterHelper.decode_coin_name(kwargs['data']['coin']), 1402 | 'value': MinterHelper.to_bip( 1403 | int.from_bytes(kwargs['data']['value'], 'big') 1404 | ) 1405 | }) 1406 | 1407 | # Populate data key values as kwargs 1408 | kwargs.update(kwargs['data']) 1409 | 1410 | return kwargs 1411 | 1412 | @classmethod 1413 | def _data_from_raw(cls, raw_data): 1414 | """ Parent method implementation """ 1415 | return { 1416 | 'pub_key': raw_data[0], 1417 | 'coin': raw_data[1], 1418 | 'value': raw_data[2] 1419 | } 1420 | 1421 | 1422 | class MinterEditCandidateTx(MinterTx): 1423 | """ Edit candidate transaction """ 1424 | 1425 | # Type of transaction 1426 | TYPE = 14 1427 | 1428 | # Fee units 1429 | COMMISSION = 10000 1430 | 1431 | def __init__(self, pub_key, reward_address, owner_address, **kwargs): 1432 | """ 1433 | Args: 1434 | pub_key (str): candidate public key 1435 | reward_address (str) 1436 | owner_address (str) 1437 | """ 1438 | 1439 | super().__init__(**kwargs) 1440 | 1441 | self.pub_key = pub_key 1442 | self.reward_address = reward_address 1443 | self.owner_address = owner_address 1444 | 1445 | def _structure_from_instance(self): 1446 | """ Override parent method to add tx special data. """ 1447 | 1448 | struct = super()._structure_from_instance() 1449 | 1450 | struct.update({ 1451 | 'type': self.TYPE, 1452 | 'data': { 1453 | 'pub_key': bytes.fromhex( 1454 | MinterHelper.prefix_remove(self.pub_key) 1455 | ), 1456 | 'reward_address': bytes.fromhex( 1457 | MinterHelper.prefix_remove(self.reward_address) 1458 | ), 1459 | 'owner_address': bytes.fromhex( 1460 | MinterHelper.prefix_remove(self.owner_address) 1461 | ) 1462 | } 1463 | }) 1464 | 1465 | return struct 1466 | 1467 | @classmethod 1468 | def _structure_to_kwargs(cls, structure): 1469 | """ Prepare decoded structure data to instance kwargs. """ 1470 | 1471 | kwargs = super()._structure_to_kwargs(structure) 1472 | 1473 | # Convert data values to verbose. 1474 | # Data will be passed as additional kwarg 1475 | kwargs['data'].update({ 1476 | 'pub_key': MinterHelper.prefix_add( 1477 | kwargs['data']['pub_key'].hex(), PREFIX_PUBKEY 1478 | ), 1479 | 'reward_address': MinterHelper.prefix_add( 1480 | kwargs['data']['reward_address'].hex(), PREFIX_ADDR 1481 | ), 1482 | 'owner_address': MinterHelper.prefix_add( 1483 | kwargs['data']['owner_address'].hex(), PREFIX_ADDR 1484 | ) 1485 | }) 1486 | 1487 | # Populate data key values as kwargs 1488 | kwargs.update(kwargs['data']) 1489 | 1490 | return kwargs 1491 | 1492 | @classmethod 1493 | def _data_from_raw(cls, raw_data): 1494 | """ Parent method implementation """ 1495 | return { 1496 | 'pub_key': raw_data[0], 1497 | 'reward_address': raw_data[1], 1498 | 'owner_address': raw_data[2] 1499 | } 1500 | 1501 | 1502 | class MinterCreateMultisigTx(MinterTx): 1503 | """ Create multi signature address transaction """ 1504 | # Type of transaction 1505 | TYPE = 12 1506 | 1507 | # Fee units 1508 | COMMISSION = 100 1509 | 1510 | def __init__(self, threshold, weights, addresses, **kwargs): 1511 | """ 1512 | Args: 1513 | threshold (int): Address threshold 1514 | weights (list(int)): List ow weights 1515 | addresses (list(str)): List of addresses 1516 | **kwargs: MinterTx kwargs 1517 | """ 1518 | self.threshold = threshold 1519 | self.weights = weights 1520 | self.addresses = addresses 1521 | 1522 | super(MinterCreateMultisigTx, self).__init__(**kwargs) 1523 | 1524 | def validate_attrs(self): 1525 | """ Overloaded to validate custom attrs """ 1526 | super(MinterCreateMultisigTx, self).validate_attrs() 1527 | 1528 | if type(self.threshold) is not int: 1529 | raise ValueError("'threshold' should be 'int'") 1530 | 1531 | if type(self.weights) not in (list, tuple) or \ 1532 | 1 > len(self.weights) > 32 or \ 1533 | any(type(w) is not int for w in self.weights) or \ 1534 | any(w > 1023 for w in self.weights): 1535 | raise ValueError( 1536 | "'weights' should be a list of max 32 integers " 1537 | "with max 1023 value" 1538 | ) 1539 | 1540 | if type(self.addresses) not in (list, tuple) or \ 1541 | 1 > len(self.addresses) > 32 or \ 1542 | any(type(a) is not str for a in self.addresses): 1543 | raise ValueError("'addresses' should be a list of max 32 strings") 1544 | 1545 | if len(self.weights) != len(self.addresses): 1546 | raise ValueError("'weights' and 'addresses' have different length") 1547 | 1548 | def _structure_from_instance(self): 1549 | """ Override parent method to add tx special data. """ 1550 | struct = super()._structure_from_instance() 1551 | struct.update({ 1552 | 'type': self.TYPE, 1553 | 'data': { 1554 | 'threshold': self.threshold if self.threshold else '', 1555 | 'weights': [w if w else '' for w in self.weights], 1556 | 'addresses': [ 1557 | bytes.fromhex(MinterHelper.prefix_remove(address)) 1558 | for address in self.addresses 1559 | ] 1560 | } 1561 | }) 1562 | 1563 | return struct 1564 | 1565 | @classmethod 1566 | def _structure_to_kwargs(cls, structure): 1567 | """ Prepare decoded structure data to instance kwargs. """ 1568 | kwargs = super()._structure_to_kwargs(structure) 1569 | 1570 | # Convert data values to verbose. 1571 | # Data will be passed as additional kwarg 1572 | kwargs['data'].update({ 1573 | 'threshold': int.from_bytes(kwargs['data']['threshold'], 'big'), 1574 | 'weights': [ 1575 | int.from_bytes(w, 'big') for w in kwargs['data']['weights'] 1576 | ], 1577 | 'addresses': [ 1578 | MinterHelper.prefix_add(address.hex(), PREFIX_ADDR) 1579 | for address in kwargs['data']['addresses'] 1580 | ] 1581 | }) 1582 | 1583 | # Populate data key values as kwargs 1584 | kwargs.update(kwargs['data']) 1585 | 1586 | return kwargs 1587 | 1588 | @classmethod 1589 | def _data_from_raw(cls, raw_data): 1590 | return { 1591 | 'threshold': raw_data[0], 1592 | 'weights': raw_data[1], 1593 | 'addresses': raw_data[2] 1594 | } 1595 | -------------------------------------------------------------------------------- /mintersdk/sdk/wallet.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: Roman Matusevich 3 | """ 4 | import hashlib 5 | import hmac 6 | 7 | import sslcrypto 8 | from mnemonic.mnemonic import Mnemonic 9 | from mintersdk import MinterHelper, PREFIX_PUBKEY, PREFIX_ADDR 10 | 11 | 12 | class MinterWallet(object): 13 | """ 14 | Minter wallet class 15 | """ 16 | 17 | # Amount of entropy bits (BIP44) 18 | entropy_bits = 128 19 | 20 | # Address path for creating wallet from the seed (BIP44) 21 | seed_address_path = "m/44'/60'/0'/0/0" 22 | 23 | # Master seed 24 | master_seed = b'Bitcoin seed' 25 | 26 | # Curve data 27 | curve = sslcrypto.ecc.get_curve('secp256k1') 28 | pub_key_len = curve._backend.public_key_length 29 | 30 | @classmethod 31 | def create(cls, mnemonic=None): 32 | """ 33 | Create Minter wallet 34 | Args: 35 | mnemonic (str): Mnemonic phrase 36 | Returns: 37 | dict 38 | """ 39 | 40 | # Create mnemonic phrase if None 41 | if not mnemonic: 42 | _mnemonic = Mnemonic(language='english') 43 | mnemonic = _mnemonic.generate(cls.entropy_bits) 44 | 45 | if len(mnemonic.split(' ')) != 12: 46 | raise Exception('Mnemonic phrase should have 12 words.') 47 | 48 | # Mnemonic to seed (bytes) 49 | seed = Mnemonic.to_seed(mnemonic, '') 50 | 51 | # Generate master key (key, hmac_key) from master seed 52 | _I = hmac.new(cls.master_seed, seed, hashlib.sha512).hexdigest() 53 | master_key = (int(_I[:64], 16), bytes.fromhex(_I[64:])) 54 | 55 | # Get child keys from master key by path 56 | keys = cls.from_path( 57 | root_key=master_key, path=cls.seed_address_path 58 | ) 59 | 60 | # Get private key 61 | private_key = keys[-1][0].to_bytes(length=32, byteorder='big').hex() 62 | # Get public key from private 63 | public_key = cls.get_public_from_private(private_key) 64 | # Get address from public key 65 | address = cls.get_address_from_public_key(public_key) 66 | 67 | return { 68 | 'address': address, 69 | 'private_key': private_key, 70 | 'mnemonic': mnemonic, 71 | 'seed': seed.hex() 72 | } 73 | 74 | @classmethod 75 | def get_public_from_private(cls, private_key): 76 | """ 77 | Get public key from private key 78 | Args: 79 | private_key (str): hex bytes of private key 80 | Returns: 81 | str 82 | """ 83 | # Get public key from private 84 | public_key = cls.curve.private_to_public( 85 | int(private_key, 16).to_bytes(length=32, byteorder='big') 86 | ) 87 | public_key = public_key.hex()[2:] 88 | 89 | return MinterHelper.prefix_add(public_key, PREFIX_PUBKEY) 90 | 91 | @classmethod 92 | def get_address_from_public_key(cls, public_key): 93 | """ 94 | Args: 95 | public_key (str) 96 | Returns: 97 | str 98 | """ 99 | # Create keccak hash 100 | _keccak = MinterHelper.keccak_hash( 101 | bytes.fromhex(MinterHelper.prefix_remove(public_key)) 102 | ) 103 | 104 | return MinterHelper.prefix_add(_keccak[-40:], PREFIX_ADDR) 105 | 106 | @staticmethod 107 | def parse_path(path): 108 | """ 109 | Parsing seed address path. 110 | Method was ported from 'two1.bitcoin.crypto' 111 | Args: 112 | path (str): Seed address path 113 | Returns: 114 | list 115 | """ 116 | if isinstance(path, str): 117 | # Remove trailing "/" 118 | p = path.rstrip("/").split("/") 119 | elif isinstance(path, bytes): 120 | p = path.decode('utf-8').rstrip("/").split("/") 121 | else: 122 | p = list(path) 123 | 124 | return p 125 | 126 | @classmethod 127 | def from_parent(cls, parent_key, index): 128 | """ 129 | Generate child private key from parent private key. 130 | Method was ported from 'two1.bitcoin.crypto'. 131 | Method is suitable only for private keys. To use full functionality, 132 | you should install 'two1' package. 133 | Args: 134 | parent_key (tuple(int, bytes)): Tuple of key and hmac_key 135 | index (int): Child index 136 | Returns: 137 | tuple(int, bytes): Child key 138 | """ 139 | 140 | if index < 0 or index > 0xffffffff: 141 | raise ValueError("index is out of range: 0 <= index <= 2**32 - 1") 142 | 143 | # Get curve n parameter. 144 | curve_n = int(cls.curve.params['n']) 145 | 146 | # Unpack parent key 147 | parent_key, hmac_key = parent_key 148 | 149 | if index & 0x80000000: 150 | hmac_data = b'\x00' + parent_key.to_bytes(length=32, byteorder='big') 151 | else: 152 | # Create default curve public key from private 153 | public_key = cls.curve.private_to_public( 154 | parent_key.to_bytes(length=32, byteorder='big') 155 | ) 156 | 157 | # Get public key coordinates 158 | x, y = cls.curve.decode_public_key(public_key) 159 | x = int.from_bytes(x, byteorder='big') 160 | y = int.from_bytes(y, byteorder='big') 161 | 162 | # Generate hmac data 163 | hmac_data = ( 164 | bytes([(y & 0x1) + 0x02]) + 165 | x.to_bytes(cls.pub_key_len, 'big') 166 | ) 167 | hmac_data += index.to_bytes(length=4, byteorder='big') 168 | 169 | I = hmac.new(hmac_key, hmac_data, hashlib.sha512).digest() 170 | Il, Ir = I[:32], I[32:] 171 | 172 | parse_Il = int.from_bytes(Il, 'big') 173 | if parse_Il >= curve_n: 174 | return None 175 | 176 | child_key = (parse_Il + parent_key) % curve_n 177 | if child_key == 0: 178 | # Incredibly unlucky choice 179 | return None 180 | 181 | return child_key, Ir 182 | 183 | @classmethod 184 | def from_path(cls, root_key, path): 185 | """ 186 | Generate keys from path. 187 | Method was ported from 'two1.bitcoin.crypto' 188 | Args: 189 | root_key (tuple(int, bytes)): Tuple of key and hmac_key 190 | path (str): Seed address path 191 | Returns: 192 | list(tuple(int, bytes)): List of tuples (key, hmac_key) 193 | """ 194 | p = cls.parse_path(path) 195 | 196 | if p[0] == "m": 197 | p = p[1:] 198 | 199 | keys = [root_key] 200 | for i in p: 201 | if isinstance(i, str): 202 | hardened = i[-1] == "'" 203 | index = int(i[:-1], 0) | 0x80000000 if hardened else int(i, 0) 204 | else: 205 | index = i 206 | k = keys[-1] 207 | 208 | keys.append(cls.from_parent(parent_key=k, index=index)) 209 | 210 | return keys 211 | -------------------------------------------------------------------------------- /mintersdk/shortcuts.py: -------------------------------------------------------------------------------- 1 | from mintersdk import MinterHelper 2 | 3 | 4 | def to_pip(value): 5 | return MinterHelper.to_pip(value) 6 | 7 | 8 | def to_bip(value): 9 | return MinterHelper.to_bip(value) 10 | -------------------------------------------------------------------------------- /mintersdk/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/U-node/minter-sdk/f205028789333f261b11f0a223fa03992731d6ea/mintersdk/test/__init__.py -------------------------------------------------------------------------------- /mintersdk/test/test_check.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mintersdk.sdk.check import MinterCheck 4 | from mintersdk.sdk.transactions import MinterTx 5 | 6 | 7 | class TestMinterCheck(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.PRIVATE_KEY = '64e27afaab363f21eec05291084367f6f1297a7b280d69d672febecda94a09ea' 11 | self.ADDRESS = 'Mxa7bc33954f1ce855ed1a8c768fdd32ed927def47' 12 | self.PASSPHRASE = 'pass' 13 | self.VALID_CHECK = 'Mcf8ae8334383002830f423f8a4d4e5400000000000000888ac7230489e800008a4d4e5400000000000000b841497c5f3e6fc182fd1a791522a9ef7576710bdfbc86fdbf165476ef220e89f9ff1380f93f2d9a2f92fdab0edc1e2605cc2c69b707cd404b2cb1522b7aba4defd5001ba083c9945169f0a7bbe596973b32dc887608780580b1d3bc7b188bedb3bd385594a047b2d5345946ed5498f5bee713f86276aac046a5fef820beaee77a9b6f9bc1df' 14 | self.VALID_PROOF = 'da021d4f84728e0d3d312a18ec84c21768e0caa12a53cb0a1452771f72b0d1a91770ae139fd6c23bcf8cec50f5f2e733eabb8482cf29ee540e56c6639aac469600' 15 | self.CHECK = MinterCheck( 16 | nonce=480, 17 | due_block=999999, 18 | coin='MNT', 19 | value=10, 20 | passphrase=self.PASSPHRASE, 21 | chain_id=MinterTx.TESTNET_CHAIN_ID, 22 | gas_coin='MNT' 23 | ) 24 | 25 | def test_check(self): 26 | check = self.CHECK.sign(self.PRIVATE_KEY) 27 | 28 | self.assertEqual(check, self.VALID_CHECK) 29 | 30 | def test_proof(self): 31 | proof = MinterCheck.proof( 32 | address=self.ADDRESS, 33 | passphrase=self.PASSPHRASE 34 | ) 35 | 36 | self.assertEqual(proof, self.VALID_PROOF) 37 | 38 | def test_fromraw(self): 39 | check = MinterCheck.from_raw(rawcheck=self.VALID_CHECK) 40 | 41 | self.assertEqual( 42 | check.owner, 43 | 'Mxce931863b9c94a526d94acd8090c1c5955a6eb4b' 44 | ) 45 | self.assertEqual(check.gas_coin, self.CHECK.gas_coin) 46 | -------------------------------------------------------------------------------- /mintersdk/test/test_deeplink.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mintersdk.sdk.transactions import MinterSendCoinTx 4 | from mintersdk.sdk.deeplink import MinterDeeplink 5 | 6 | 7 | class TestDeeplink(unittest.TestCase): 8 | def test_fulltx(self): 9 | tx = MinterSendCoinTx( 10 | coin='BIP', to='Mx18467bbb64a8edf890201d526c35957d82be3d95', 11 | value=1.23456789, nonce=1, gas_coin='MNT', gas_price=1, 12 | payload='Check payload' 13 | ) 14 | deeplink = MinterDeeplink(tx=tx) 15 | 16 | self.assertEqual( 17 | MinterDeeplink.BASE_URL + '/-EcBqumKQklQAAAAAAAAAJQYRnu7ZKjt-JAgHVJsNZV9gr49lYgRIhD0do20AI1DaGVjayBwYXlsb2FkAQGKTU5UAAAAAAAAAA', 18 | deeplink.generate() 19 | ) 20 | 21 | def test_data_part_with_payload(self): 22 | tx = MinterSendCoinTx( 23 | coin='MNT', to='Mx7633980c000139dd3bd24a3f54e06474fa941e16', 24 | value=10, nonce=1, gas_coin='ASD', gas_price=1, 25 | payload='custom message' 26 | ) 27 | deeplink = MinterDeeplink(tx=tx) 28 | deeplink.nonce = '' 29 | deeplink.gas_price = '' 30 | 31 | self.assertEqual( 32 | MinterDeeplink.BASE_URL + '/-EgBqumKTU5UAAAAAAAAAJR2M5gMAAE53TvSSj9U4GR0-pQeFoiKxyMEiegAAI5jdXN0b20gbWVzc2FnZYCAikFTRAAAAAAAAAA', 33 | deeplink.generate() 34 | ) 35 | 36 | def test_data_only(self): 37 | tx = MinterSendCoinTx( 38 | coin='BIP', to='Mx18467bbb64a8edf890201d526c35957d82be3d95', 39 | value=1.23456789, nonce=1, gas_coin='MNT', gas_price=1, 40 | payload='Hello World' 41 | ) 42 | deeplink = MinterDeeplink(tx=tx, data_only=True) 43 | 44 | self.assertEqual( 45 | MinterDeeplink.BASE_URL + '/8AGq6YpCSVAAAAAAAAAAlBhGe7tkqO34kCAdUmw1lX2Cvj2ViBEiEPR2jbQAgICAgA', 46 | deeplink.generate() 47 | ) 48 | -------------------------------------------------------------------------------- /mintersdk/test/test_transactions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import base64 3 | import decimal 4 | 5 | from mintersdk.sdk.transactions import ( 6 | MinterTx, MinterDelegateTx, MinterSendCoinTx, MinterBuyCoinTx, 7 | MinterCreateCoinTx, MinterDeclareCandidacyTx, MinterEditCandidateTx, 8 | MinterRedeemCheckTx, MinterSellAllCoinTx, MinterSellCoinTx, 9 | MinterSetCandidateOffTx, MinterSetCandidateOnTx, MinterUnbondTx, 10 | MinterMultiSendCoinTx, MinterCreateMultisigTx 11 | ) 12 | 13 | 14 | class TestMinterTx(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.SIGNED_TX = 'f8900102018a4d4e540000000000000007b6f5a00eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a438a4d4e5400000000000000888ac7230489e80000808001b845f8431ba01c2c8f702d80cf64da1e9bf1f07a52e2fee8721aebe419aa9f62260a98983f89a07ed297d71d9dc37a57ffe9bb16915dccc703d8c09f30da8aadb9d5dbab8c7da9' 18 | self.PUBLIC_KEY = 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43' 19 | self.TX_FROM = 'Mx9f7fd953c2c69044b901426831ed03ee0bd0597a' 20 | self.TX_TYPE = 7 21 | self.TX_STAKE = 10 22 | self.TX_PAYLOAD = '🔳' # 4 bytes 23 | self.tx = MinterTx.from_raw(self.SIGNED_TX) 24 | 25 | def test_instance(self): 26 | self.assertIsInstance(self.tx, MinterDelegateTx) 27 | 28 | def test_public_key(self): 29 | self.assertEqual(self.tx.pub_key, self.PUBLIC_KEY) 30 | 31 | def test_from_mx(self): 32 | self.assertEqual(self.tx.from_mx, self.TX_FROM) 33 | 34 | def test_type(self): 35 | self.assertEqual(self.tx.type, self.TX_TYPE) 36 | 37 | def test_stake(self): 38 | self.assertEqual(self.tx.stake, self.TX_STAKE) 39 | 40 | 41 | class TestMinterBuyCoinTx(unittest.TestCase): 42 | 43 | def setUp(self): 44 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 45 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 46 | self.SIGNED_TX = 'f8830102018a4d4e540000000000000004a9e88a54455354000000000000880de0b6b3a76400008a4d4e5400000000000000880de0b6b3a7640000808001b845f8431ca04ee095a20ca58062a5758e2a6d3941857daa8943b5873c57f111190ca88dbc56a01148bf2fcc721ca353105e4f4a3419bec471d7ae08173f443a28c3ae6d27018a' 47 | self.TX = MinterBuyCoinTx(**{ 48 | 'nonce': 1, 49 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 50 | 'gas_coin': 'MNT', 51 | 'coin_to_buy': 'TEST', 52 | 'value_to_buy': 1, 53 | 'coin_to_sell': 'MNT', 54 | 'max_value_to_sell': 1 55 | }) 56 | 57 | def test_valid_tx(self): 58 | """ 59 | Is tx instance of needed TX class. 60 | """ 61 | 62 | self.assertIsInstance(self.TX, MinterBuyCoinTx) 63 | 64 | def test_sign_tx(self): 65 | """ 66 | Sign transaction and check signed transaction 67 | """ 68 | self.TX.sign(self.PRIVATE_KEY) 69 | 70 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 71 | 72 | def test_sign_with_signature(self): 73 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 74 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 75 | self.TX.sign(signature=signature) 76 | 77 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 78 | 79 | def test_from_raw(self): 80 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 81 | 82 | self.assertEqual(tx.from_mx, self.FROM) 83 | self.assertEqual(tx.coin_to_buy, self.TX.coin_to_buy) 84 | self.assertEqual(tx.value_to_buy, self.TX.value_to_buy) 85 | self.assertEqual(tx.coin_to_sell, self.TX.coin_to_sell) 86 | self.assertEqual(tx.max_value_to_sell, self.TX.max_value_to_sell) 87 | 88 | 89 | class TestMinterCreateCoinTx(unittest.TestCase): 90 | 91 | def setUp(self): 92 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 93 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 94 | self.SIGNED_TX = 'f88f0102018a4d4e540000000000000005b5f48a535550455220544553548a5350525445535400000089056bc75e2d63100000888ac7230489e800000a893635c9adc5dea00000808001b845f8431ca0ccfabd9283d27cf7978bca378e0cc7dc69a39ff3bdc56707fa2d552655f9290da0226057221cbaef35696c9315cd29e783d3c66d842d0a3948a922abb42ca0dabe' 95 | self.TX = MinterCreateCoinTx(**{ 96 | 'nonce': 1, 97 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 98 | 'gas_coin': 'MNT', 99 | 'name': 'SUPER TEST', 100 | 'symbol': 'SPRTEST', 101 | 'initial_amount': 100, 102 | 'initial_reserve': 10, 103 | 'crr': 10, 104 | 'max_supply': 1000 105 | }) 106 | 107 | def test_valid_tx(self): 108 | """ 109 | Is tx instance of needed TX class. 110 | """ 111 | 112 | self.assertIsInstance(self.TX, MinterCreateCoinTx) 113 | 114 | def test_sign_tx(self): 115 | """ 116 | Sign transaction and check signed transaction 117 | """ 118 | self.TX.sign(self.PRIVATE_KEY) 119 | 120 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 121 | 122 | def test_sign_with_signature(self): 123 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 124 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 125 | self.TX.sign(signature=signature) 126 | 127 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 128 | 129 | def test_from_raw(self): 130 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 131 | 132 | self.assertEqual(tx.from_mx, self.FROM) 133 | self.assertEqual(tx.name, self.TX.name) 134 | self.assertEqual(tx.symbol, self.TX.symbol) 135 | self.assertEqual(tx.initial_amount, self.TX.initial_amount) 136 | self.assertEqual(tx.initial_reserve, self.TX.initial_reserve) 137 | self.assertEqual(tx.crr, self.TX.crr) 138 | self.assertEqual(tx.max_supply, self.TX.max_supply) 139 | 140 | 141 | class TestMinterDeclareCandidacyTx(unittest.TestCase): 142 | 143 | def setUp(self): 144 | self.PRIVATE_KEY = '6e1df6ec69638d152f563c5eca6c13cdb5db4055861efc11ec1cdd578afd96bf' 145 | self.SIGNED_TX = 'f8a80102018a4d4e540000000000000006b84df84b949f7fd953c2c69044b901426831ed03ee0bd0597aa00eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a430a8a4d4e5400000000000000884563918244f40000808001b845f8431ca0c379230cbe09103b31983402c9138ad29d839bcecee70e11ac9bf5cfe70850d9a06c92bfb9a627bfaefc3ad46fc60ff1fdc42efe0e8805d57f20795a403c91e8bd' 146 | self.TX = MinterDeclareCandidacyTx(**{ 147 | 'nonce': 1, 148 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 149 | 'gas_coin': 'MNT', 150 | 'address': 'Mx9f7fd953c2c69044b901426831ed03ee0bd0597a', 151 | 'pub_key': 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43', 152 | 'commission': 10, 153 | 'coin': 'MNT', 154 | 'stake': 5 155 | }) 156 | 157 | def test_valid_tx(self): 158 | """ 159 | Is tx instance of needed TX class. 160 | """ 161 | 162 | self.assertIsInstance(self.TX, MinterDeclareCandidacyTx) 163 | 164 | def test_sign_tx(self): 165 | """ 166 | Sign transaction and check signed transaction 167 | """ 168 | self.TX.sign(self.PRIVATE_KEY) 169 | 170 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 171 | 172 | def test_sign_with_signature(self): 173 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 174 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 175 | self.TX.sign(signature=signature) 176 | 177 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 178 | 179 | def test_from_raw(self): 180 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 181 | 182 | self.assertEqual(tx.from_mx, self.TX.address) 183 | self.assertEqual(tx.address, self.TX.address) 184 | self.assertEqual(tx.pub_key, self.TX.pub_key) 185 | self.assertEqual(tx.commission, self.TX.commission) 186 | self.assertEqual(tx.coin, self.TX.coin) 187 | self.assertEqual(tx.stake, self.TX.stake) 188 | 189 | 190 | class TestMinterDelegateTx(unittest.TestCase): 191 | 192 | def setUp(self): 193 | self.PRIVATE_KEY = '6e1df6ec69638d152f563c5eca6c13cdb5db4055861efc11ec1cdd578afd96bf' 194 | self.PUBLIC_KEY = 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43' 195 | self.SIGNED_TX = 'f8900102018a4d4e540000000000000007b6f5a00eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a438a4d4e5400000000000000888ac7230489e80000808001b845f8431ba01c2c8f702d80cf64da1e9bf1f07a52e2fee8721aebe419aa9f62260a98983f89a07ed297d71d9dc37a57ffe9bb16915dccc703d8c09f30da8aadb9d5dbab8c7da9' 196 | 197 | self.TX = MinterDelegateTx(**{ 198 | 'nonce': 1, 199 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 200 | 'gas_coin': 'MNT', 201 | 'pub_key': self.PUBLIC_KEY, 202 | 'coin': 'MNT', 203 | 'stake': 10 204 | }) 205 | 206 | def test_valid_tx(self): 207 | """ 208 | Is tx instance of needed TX class. 209 | """ 210 | 211 | self.assertIsInstance(self.TX, MinterDelegateTx) 212 | 213 | def test_sign_tx(self): 214 | """ 215 | Sign transaction and check signed transaction 216 | """ 217 | self.TX.sign(self.PRIVATE_KEY) 218 | 219 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 220 | 221 | def test_sign_with_signature(self): 222 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 223 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 224 | self.TX.sign(signature=signature) 225 | 226 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 227 | 228 | def test_from_raw(self): 229 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 230 | 231 | self.assertEqual(tx.pub_key, self.PUBLIC_KEY) 232 | self.assertEqual(tx.coin, self.TX.coin) 233 | self.assertEqual(tx.stake, self.TX.stake) 234 | 235 | 236 | class TestMinterRedeemCheckTx(unittest.TestCase): 237 | 238 | def setUp(self): 239 | self.PRIVATE_KEY = '05ddcd4e6f7d248ed1388f0091fe345bf9bf4fc2390384e26005e7675c98b3c1' 240 | self.SIGNED_TX = 'f9013f0102018a4d4e540000000000000009b8e4f8e2b89df89b01830f423f8a4d4e5400000000000000843b9aca00b8419b3beac2c6ad88a8bd54d24912754bb820e58345731cb1b9bc0885ee74f9e50a58a80aa990a29c98b05541b266af99d3825bb1e5ed4e540c6e2f7c9b40af9ecc011ca00f7ba6d0aa47d74274b960fba02be03158d0374b978dcaa5f56fc7cf1754f821a019a829a3b7bba2fc290f5c96e469851a3876376d6a6a4df937327b3a5e9e8297b841da021d4f84728e0d3d312a18ec84c21768e0caa12a53cb0a1452771f72b0d1a91770ae139fd6c23bcf8cec50f5f2e733eabb8482cf29ee540e56c6639aac469600808001b845f8431ba009493b3296a085a27f2bc015ad5c1cc644ba21bdce1b78a49e987227f24a87a3a07187da48b6ea528d372ed33923f5d74011f56cc2db3cab2cf5b4bbab97990373' 241 | self.TX = MinterRedeemCheckTx(**{ 242 | 'nonce': 1, 243 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 244 | 'gas_coin': 'MNT', 245 | 'check': 'Mcf89b01830f423f8a4d4e5400000000000000843b9aca00b8419b3beac2c6ad88a8bd54d24912754bb820e58345731cb1b9bc0885ee74f9e50a58a80aa990a29c98b05541b266af99d3825bb1e5ed4e540c6e2f7c9b40af9ecc011ca00f7ba6d0aa47d74274b960fba02be03158d0374b978dcaa5f56fc7cf1754f821a019a829a3b7bba2fc290f5c96e469851a3876376d6a6a4df937327b3a5e9e8297', 246 | 'proof': 'da021d4f84728e0d3d312a18ec84c21768e0caa12a53cb0a1452771f72b0d1a91770ae139fd6c23bcf8cec50f5f2e733eabb8482cf29ee540e56c6639aac469600' 247 | }) 248 | 249 | def test_valid_tx(self): 250 | """ 251 | Is tx instance of needed TX class. 252 | """ 253 | 254 | self.assertIsInstance(self.TX, MinterRedeemCheckTx) 255 | 256 | def test_sign_tx(self): 257 | """ 258 | Sign transaction and check signed transaction 259 | """ 260 | self.TX.sign(self.PRIVATE_KEY) 261 | 262 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 263 | 264 | def test_sign_with_signature(self): 265 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 266 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 267 | self.TX.sign(signature=signature) 268 | 269 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 270 | 271 | def test_from_raw(self): 272 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 273 | 274 | self.assertEqual(tx.check, self.TX.check) 275 | self.assertEqual(tx.proof, self.TX.proof) 276 | 277 | 278 | class TestMinterSellAllCoinTx(unittest.TestCase): 279 | 280 | def setUp(self): 281 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 282 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 283 | self.SIGNED_TX = 'f87a0102018a4d4e540000000000000003a0df8a4d4e54000000000000008a54455354000000000000880de0b6b3a7640000808001b845f8431ca0b10794a196b6ad2f94e6162613ca9538429dd49ca493594ba9d99f80d2499765a03c1d78e9e04f57336691e8812a16faccb00bf92ac817ab61cd9bf001e9380d47' 284 | self.TX = MinterSellAllCoinTx(**{ 285 | 'nonce': 1, 286 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 287 | 'gas_coin': 'MNT', 288 | 'coin_to_sell': 'MNT', 289 | 'coin_to_buy': 'TEST', 290 | 'min_value_to_buy': 1 291 | }) 292 | 293 | def test_valid_tx(self): 294 | """ 295 | Is tx instance of needed TX class. 296 | """ 297 | 298 | self.assertIsInstance(self.TX, MinterSellAllCoinTx) 299 | 300 | def test_sign_tx(self): 301 | """ 302 | Sign transaction and check signed transaction 303 | """ 304 | self.TX.sign(self.PRIVATE_KEY) 305 | 306 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 307 | 308 | def test_sign_with_signature(self): 309 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 310 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 311 | self.TX.sign(signature=signature) 312 | 313 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 314 | 315 | def test_from_raw(self): 316 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 317 | 318 | self.assertEqual(tx.from_mx, self.FROM) 319 | self.assertEqual(tx.coin_to_sell, self.TX.coin_to_sell) 320 | self.assertEqual(tx.coin_to_buy, self.TX.coin_to_buy) 321 | self.assertEqual(tx.min_value_to_buy, self.TX.min_value_to_buy) 322 | 323 | 324 | class TestMinterSellCoinTx(unittest.TestCase): 325 | 326 | def setUp(self): 327 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 328 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 329 | self.SIGNED_TX = 'f8830102018a4d4e540000000000000002a9e88a4d4e5400000000000000880de0b6b3a76400008a54455354000000000000880de0b6b3a7640000808001b845f8431ba0e34be907a18acb5a1aed263ef419f32f5adc6e772b92f949906b497bba557df3a0291d7704980994f7a6f5950ca84720746b5928f21c3cfc5a5fbca2a9f4d35db0' 330 | self.TX = MinterSellCoinTx(**{ 331 | 'nonce': 1, 332 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 333 | 'gas_coin': 'MNT', 334 | 'coin_to_sell': 'MNT', 335 | 'value_to_sell': 1, 336 | 'coin_to_buy': 'TEST', 337 | 'min_value_to_buy': 1 338 | }) 339 | 340 | def test_valid_tx(self): 341 | """ 342 | Is tx instance of needed TX class. 343 | """ 344 | 345 | self.assertIsInstance(self.TX, MinterSellCoinTx) 346 | 347 | def test_sign_tx(self): 348 | """ 349 | Sign transaction and check signed transaction 350 | """ 351 | self.TX.sign(self.PRIVATE_KEY) 352 | 353 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 354 | 355 | def test_sign_with_signature(self): 356 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 357 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 358 | self.TX.sign(signature=signature) 359 | 360 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 361 | 362 | def test_from_raw(self): 363 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 364 | 365 | self.assertEqual(tx.from_mx, self.FROM) 366 | self.assertEqual(tx.coin_to_sell, self.TX.coin_to_sell) 367 | self.assertEqual(tx.value_to_sell, self.TX.value_to_sell) 368 | self.assertEqual(tx.coin_to_buy, self.TX.coin_to_buy) 369 | self.assertEqual(tx.min_value_to_buy, self.TX.min_value_to_buy) 370 | 371 | 372 | class TestMinterSendTx(unittest.TestCase): 373 | 374 | def setUp(self): 375 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 376 | self.TO = 'Mx1b685a7c1e78726c48f619c497a07ed75fe00483' 377 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 378 | self.SIGNED_TX = 'f8840102018a4d4e540000000000000001aae98a4d4e5400000000000000941b685a7c1e78726c48f619c497a07ed75fe00483880de0b6b3a7640000808001b845f8431ca01f36e51600baa1d89d2bee64def9ac5d88c518cdefe45e3de66a3cf9fe410de4a01bc2228dc419a97ded0efe6848de906fbe6c659092167ef0e7dcb8d15024123a' 379 | self.TX = MinterSendCoinTx(**{ 380 | 'nonce': 1, 381 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 382 | 'gas_coin': 'MNT', 383 | 'to': self.TO, 384 | 'coin': 'MNT', 385 | 'value': 1 386 | }) 387 | 388 | def test_valid_tx(self): 389 | """ 390 | Is tx instance of needed TX class. 391 | """ 392 | 393 | self.assertIsInstance(self.TX, MinterSendCoinTx) 394 | 395 | def test_sign_tx(self): 396 | """ 397 | Sign transaction and check signed transaction 398 | """ 399 | self.TX.sign(self.PRIVATE_KEY) 400 | 401 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 402 | 403 | def test_sign_with_signature(self): 404 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 405 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 406 | self.TX.sign(signature=signature) 407 | 408 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 409 | 410 | def test_from_raw(self): 411 | tx = MinterTx.from_raw(self.SIGNED_TX) 412 | 413 | self.assertEqual(tx.from_mx, self.FROM) 414 | self.assertEqual(tx.to, self.TX.to) 415 | self.assertEqual(tx.coin, self.TX.coin) 416 | self.assertEqual(tx.value, self.TX.value) 417 | 418 | 419 | class TestMinterSetCandidateOffTx(unittest.TestCase): 420 | 421 | def setUp(self): 422 | self.PRIVATE_KEY = '05ddcd4e6f7d248ed1388f0091fe345bf9bf4fc2390384e26005e7675c98b3c1' 423 | self.SIGNED_TX = 'f87c0102018a4d4e54000000000000000ba2e1a00eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43808001b845f8431ca02ac45817f167c34b55b8afa0b6d9692be28e2aa41dd28a134663d1f5bebb5ad8a06d5f161a625701d506db20c497d24e9939c2e342a6ff7d724cb1962267bd4ba5' 424 | self.TX = MinterSetCandidateOffTx(**{ 425 | 'nonce': 1, 426 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 427 | 'gas_coin': 'MNT', 428 | 'pub_key': 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43' 429 | }) 430 | 431 | def test_valid_tx(self): 432 | """ 433 | Is tx instance of needed TX class. 434 | """ 435 | 436 | self.assertIsInstance(self.TX, MinterSetCandidateOffTx) 437 | 438 | def test_sign_tx(self): 439 | """ 440 | Sign transaction and check signed transaction 441 | """ 442 | self.TX.sign(self.PRIVATE_KEY) 443 | 444 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 445 | 446 | def test_sign_with_signature(self): 447 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 448 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 449 | self.TX.sign(signature=signature) 450 | 451 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 452 | 453 | def test_from_raw(self): 454 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 455 | 456 | self.assertEqual(tx.pub_key, self.TX.pub_key) 457 | 458 | 459 | class TestMinterSetCandidateOnTx(unittest.TestCase): 460 | 461 | def setUp(self): 462 | self.PRIVATE_KEY = '05ddcd4e6f7d248ed1388f0091fe345bf9bf4fc2390384e26005e7675c98b3c1' 463 | self.SIGNED_TX = 'f87c0102018a4d4e54000000000000000aa2e1a00eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43808001b845f8431ba0095aed433171fe5ac385ccd299507bdcad3dd2269794fd0d14d4f58327ddc87ea046ec7e4f8f9b477a1255485f36e0567e62283723ecc5a0bd1e5d201e53e85245' 464 | self.TX = MinterSetCandidateOnTx(**{ 465 | 'nonce': 1, 466 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 467 | 'gas_coin': 'MNT', 468 | 'pub_key': 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43' 469 | }) 470 | 471 | def test_valid_tx(self): 472 | """ 473 | Is tx instance of needed TX class. 474 | """ 475 | 476 | self.assertIsInstance(self.TX, MinterSetCandidateOnTx) 477 | 478 | def test_sign_tx(self): 479 | """ 480 | Sign transaction and check signed transaction 481 | """ 482 | self.TX.sign(self.PRIVATE_KEY) 483 | 484 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 485 | 486 | def test_sign_with_signature(self): 487 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 488 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 489 | self.TX.sign(signature=signature) 490 | 491 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 492 | 493 | def test_from_raw(self): 494 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 495 | 496 | self.assertEqual(tx.pub_key, self.TX.pub_key) 497 | 498 | 499 | class TestMinterUnbondTx(unittest.TestCase): 500 | 501 | def setUp(self): 502 | self.PRIVATE_KEY = '6e1df6ec69638d152f563c5eca6c13cdb5db4055861efc11ec1cdd578afd96bf' 503 | self.SIGNED_TX = 'f88f0102018a4d4e540000000000000008b6f5a00eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a438a4d4e5400000000000000888ac7230489e80000808001b844f8421ca0ff5766c85847b37a276f3f9d027fb7c99745920fa395c7bd399cedd8265c5e1d9f791bcdfe4d1bc1e73ada7bf833103c828f22d83189dad2b22ad28a54aacf2a' 504 | self.TX = MinterUnbondTx(**{ 505 | 'nonce': 1, 506 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 507 | 'gas_coin': 'MNT', 508 | 'pub_key': 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43', 509 | 'coin': 'MNT', 510 | 'value': 10 511 | }) 512 | 513 | def test_valid_tx(self): 514 | """ 515 | Is tx instance of needed TX class. 516 | """ 517 | 518 | self.assertIsInstance(self.TX, MinterUnbondTx) 519 | 520 | def test_sign_tx(self): 521 | """ 522 | Sign transaction and check signed transaction 523 | """ 524 | self.TX.sign(self.PRIVATE_KEY) 525 | 526 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 527 | 528 | def test_sign_with_signature(self): 529 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 530 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 531 | self.TX.sign(signature=signature) 532 | 533 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 534 | 535 | def test_from_raw(self): 536 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 537 | 538 | self.assertEqual(tx.pub_key, self.TX.pub_key) 539 | self.assertEqual(tx.coin, self.TX.coin) 540 | self.assertEqual(tx.value, self.TX.value) 541 | 542 | 543 | class TestMinterEditCandidateTx(unittest.TestCase): 544 | 545 | def setUp(self): 546 | self.FROM = 'Mxa879439b0a29ecc7c5a0afe54b9eb3c22dbde8d9' 547 | self.PRIVATE_KEY = 'a3fb55450f53dbbf4f2494280188f7f0cd51a7b51ec27ed49ed364d920e326ba' 548 | self.SIGNED_TX = 'f8a80102018a4d4e54000000000000000eb84df84ba04ae1ee73e6136c85b0ca933a9a1347758a334885f10b3238398a67ac2eb153b89489e5dc185e6bab772ac8e00cf3fb3f4cb0931c4794e731fcddd37bb6e72286597d22516c8ba3ddffa0808001b845f8431ca0421470f27f78231b669c1bf1fcc56168954d64fbb7dc3ff021bab01311fab6eaa075e86365d98c87e806fcbc5c542792f569e19d8ae7af671d9ba4679acc86d35e' 549 | self.TX = MinterEditCandidateTx(**{ 550 | 'nonce': 1, 551 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 552 | 'gas_coin': 'MNT', 553 | 'pub_key': 'Mp4ae1ee73e6136c85b0ca933a9a1347758a334885f10b3238398a67ac2eb153b8', 554 | 'reward_address': 'Mx89e5dc185e6bab772ac8e00cf3fb3f4cb0931c47', 555 | 'owner_address': 'Mxe731fcddd37bb6e72286597d22516c8ba3ddffa0' 556 | }) 557 | 558 | def test_valid_tx(self): 559 | """ 560 | Is tx instance of needed TX class. 561 | """ 562 | 563 | self.assertIsInstance(self.TX, MinterEditCandidateTx) 564 | 565 | def test_sign_tx(self): 566 | """ 567 | Sign transaction and check signed transaction 568 | """ 569 | self.TX.sign(self.PRIVATE_KEY) 570 | 571 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 572 | 573 | def test_sign_with_signature(self): 574 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 575 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 576 | self.TX.sign(signature=signature) 577 | 578 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 579 | 580 | def test_from_raw(self): 581 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 582 | 583 | self.assertEqual(tx.from_mx, self.FROM) 584 | self.assertEqual(tx.pub_key, self.TX.pub_key) 585 | self.assertEqual(tx.reward_address, self.TX.reward_address) 586 | self.assertEqual(tx.owner_address, self.TX.owner_address) 587 | 588 | 589 | class TestMinterMultiSendCoinTx(unittest.TestCase): 590 | 591 | def setUp(self): 592 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 593 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 594 | self.SIGNED_TX = 'f8b30102018a4d4e54000000000000000db858f856f854e98a4d4e540000000000000094fe60014a6e9ac91618f5d1cab3fd58cded61ee9988016345785d8a0000e98a4d4e540000000000000094ddab6281766ad86497741ff91b6b48fe85012e3c8802c68af0bb140000808001b845f8431ca0b15dcf2e013df1a2aea02e36a17af266d8ee129cdcb3e881d15b70c9457e7571a0226af7bdaca9d42d6774c100b22e0c7ba4ec8dd664d17986318e905613013283' 595 | self.TX = MinterMultiSendCoinTx(**{ 596 | 'nonce': 1, 597 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 598 | 'gas_coin': 'MNT', 599 | 'txs': [ 600 | { 601 | 'coin': 'MNT', 602 | 'to': 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', 603 | 'value': decimal.Decimal('0.1') 604 | }, 605 | { 606 | 'coin': 'MNT', 607 | 'to': 'Mxddab6281766ad86497741ff91b6b48fe85012e3c', 608 | 'value': decimal.Decimal('0.2') 609 | } 610 | ] 611 | }) 612 | 613 | def test_valid_tx(self): 614 | """ 615 | Is tx instance of needed TX class. 616 | """ 617 | 618 | self.assertIsInstance(self.TX, MinterMultiSendCoinTx) 619 | 620 | def test_sign_tx(self): 621 | """ 622 | Sign transaction and check signed transaction 623 | """ 624 | self.TX.sign(self.PRIVATE_KEY) 625 | 626 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 627 | 628 | def test_from_raw(self): 629 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 630 | 631 | self.assertEqual(tx.from_mx, self.FROM) 632 | self.assertEqual(tx.txs, self.TX.txs) 633 | 634 | def test_sign_with_signature(self): 635 | self.TX.signature_type = MinterTx.SIGNATURE_SINGLE_TYPE 636 | signature = self.TX.generate_signature(self.PRIVATE_KEY) 637 | self.TX.sign(signature=signature) 638 | 639 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 640 | 641 | 642 | class TestTxFees(unittest.TestCase): 643 | 644 | def setUp(self): 645 | self.TO = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 646 | self.PRIVATE_KEY = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 647 | self.TX_PAYLOAD_UTF = '🔳' # 4 bytes 648 | self.EXPECTED_SEND_COIN_FEE = 18000000000000000 649 | 650 | self.MULTISEND_RECIPIENTS = [ 651 | { 652 | 'coin': 'MNT', 653 | 'to': 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', 654 | 'value': 0.1 655 | }, 656 | { 657 | 'coin': 'MNT', 658 | 'to': 'Mxddab6281766ad86497741ff91b6b48fe85012e3c', 659 | 'value': 0.2 660 | } 661 | ] 662 | self.EXPECTED_MULTISEND_FEE = 15000000000000000 663 | 664 | def test_payload_fee(self): 665 | tx = MinterSendCoinTx(**{ 666 | 'nonce': 1, 667 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 668 | 'gas_coin': 'MNT', 669 | 'to': self.TO, 670 | 'coin': 'MNT', 671 | 'value': 1, 672 | 'payload': self.TX_PAYLOAD_UTF 673 | }) 674 | tx.sign(self.PRIVATE_KEY) 675 | actual_fee = tx.get_fee() 676 | self.assertEqual(self.EXPECTED_SEND_COIN_FEE, actual_fee) 677 | 678 | def test_multisend_fee(self): 679 | tx = MinterMultiSendCoinTx(**{ 680 | 'nonce': 1, 681 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 682 | 'gas_coin': 'MNT', 683 | 'txs': self.MULTISEND_RECIPIENTS 684 | }) 685 | tx.sign(self.PRIVATE_KEY) 686 | actual_fee = tx.get_fee() 687 | self.assertEqual(self.EXPECTED_MULTISEND_FEE, actual_fee) 688 | 689 | 690 | class TestMinterSendMultisigTx(unittest.TestCase): 691 | 692 | def setUp(self): 693 | self.PRIVATE_KEYS = [ 694 | 'b354c3d1d456d5a1ddd65ca05fd710117701ec69d82dac1858986049a0385af9', 695 | '38b7dfb77426247aed6081f769ed8f62aaec2ee2b38336110ac4f7484478dccb', 696 | '94c0915734f92dd66acfdc48f82b1d0b208efd544fe763386160ec30c968b4af' 697 | ] 698 | self.TO = 'Mxd82558ea00eb81d35f2654953598f5d51737d31d' 699 | self.FROM = 'Mxdb4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2' 700 | self.SIGNED_TX = 'f901270102018a4d4e540000000000000001aae98a4d4e540000000000000094d82558ea00eb81d35f2654953598f5d51737d31d880de0b6b3a7640000808002b8e8f8e694db4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2f8cff8431ca0a116e33d2fea86a213577fc9dae16a7e4cadb375499f378b33cddd1d4113b6c1a021ee1e9eb61bbd24233a0967e1c745ab23001cf8816bb217d01ed4595c6cb2cdf8431ca0f7f9c7a6734ab2db210356161f2d012aa9936ee506d88d8d0cba15ad6c84f8a7a04b71b87cbbe7905942de839211daa984325a15bdeca6eea75e5d0f28f9aaeef8f8431ba0d8c640d7605034eefc8870a6a3d1c22e2f589a9319288342632b1c4e6ce35128a055fe3f93f31044033fe7b07963d547ac50bccaac38a057ce61665374c72fb454' 701 | self.TX = MinterSendCoinTx(**{ 702 | 'nonce': 1, 703 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 704 | 'gas_coin': 'MNT', 705 | 'to': self.TO, 706 | 'coin': 'MNT', 707 | 'value': 1 708 | }) 709 | 710 | def test_valid_tx(self): 711 | """ 712 | Is tx instance of needed TX class. 713 | """ 714 | 715 | self.assertIsInstance(self.TX, MinterSendCoinTx) 716 | 717 | def test_sign_tx(self): 718 | """ 719 | Sign transaction and check signed transaction 720 | """ 721 | self.TX.sign(private_key=self.PRIVATE_KEYS, ms_address=self.FROM) 722 | 723 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 724 | 725 | def test_from_raw(self): 726 | tx = MinterTx.from_raw(self.SIGNED_TX) 727 | 728 | self.assertEqual(tx.from_mx, self.FROM) 729 | self.assertEqual(tx.to, self.TX.to) 730 | self.assertEqual(tx.coin, self.TX.coin) 731 | self.assertEqual(tx.value, self.TX.value) 732 | 733 | def test_add_signature(self): 734 | # Sign tx with 2 of 3 private keys 735 | self.TX.sign(private_key=self.PRIVATE_KEYS[:2], ms_address=self.FROM) 736 | # Add signature by 3rd private key 737 | self.TX = MinterTx.add_signature(self.TX.signed_tx, self.PRIVATE_KEYS[2]) 738 | 739 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 740 | 741 | def test_sign_with_signature(self): 742 | # Set signature type for transaction 743 | self.TX.signature_type = MinterTx.SIGNATURE_MULTI_TYPE 744 | 745 | # Generate signatures 746 | signatures = [] 747 | for pk in self.PRIVATE_KEYS: 748 | signature = self.TX.generate_signature(private_key=pk) 749 | signatures.append(signature) 750 | 751 | # Sign transaction with signatures 752 | self.TX.sign(signature=signatures, ms_address=self.FROM) 753 | 754 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 755 | 756 | def test_sign_with_pk_and_signature(self): 757 | # Set signature type for transaction 758 | self.TX.signature_type = MinterTx.SIGNATURE_MULTI_TYPE 759 | 760 | # Generate 1 signature 761 | signatures = [] 762 | for pk in self.PRIVATE_KEYS: 763 | signatures.append(self.TX.generate_signature(private_key=pk)) 764 | 765 | # Sign transaction with pks and signature 766 | self.TX.sign(private_key=self.PRIVATE_KEYS[:2], signature=signatures[2], ms_address=self.FROM) 767 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 768 | 769 | self.TX.sign(private_key=self.PRIVATE_KEYS[0], signature=signatures[1:], ms_address=self.FROM) 770 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 771 | 772 | 773 | class TestPayloadsFromRaw(unittest.TestCase): 774 | def setUp(self): 775 | self.TO = 'Mxd82558ea00eb81d35f2654953598f5d51737d31d' 776 | self.FROM = 'Mx31e61a05adbd13c6b625262704bc305bf7725026' 777 | self.PK = '07bc17abdcee8b971bb8723e36fe9d2523306d5ab2d683631693238e0f9df142' 778 | self.TX = MinterSendCoinTx( 779 | nonce=1, gas_coin='mnt', to=self.TO, coin='mnt', value=1 780 | ) 781 | self.TX_DECODED = None 782 | 783 | def sign_and_decode(self, payload): 784 | self.TX.payload = payload 785 | self.TX.sign(private_key=self.PK) 786 | self.TX_DECODED = MinterTx.from_raw(self.TX.signed_tx) 787 | 788 | def test_hex_like(self): 789 | payload = 'fff' 790 | self.sign_and_decode(payload) 791 | 792 | self.assertEqual(payload, self.TX_DECODED.payload) 793 | self.assertEqual(self.FROM, self.TX_DECODED.from_mx) 794 | 795 | def test_str_bytes(self): 796 | payload = '🔳' 797 | self.sign_and_decode(payload) 798 | 799 | self.assertEqual(payload, self.TX_DECODED.payload) 800 | self.assertEqual(self.FROM, self.TX_DECODED.from_mx) 801 | 802 | def test_raw_bytes(self): 803 | payload = b'\xff\xff\xff' 804 | self.sign_and_decode(payload) 805 | 806 | self.assertEqual(payload, self.TX_DECODED.payload) 807 | self.assertEqual(self.FROM, self.TX_DECODED.from_mx) 808 | 809 | 810 | class TestFromBase64(unittest.TestCase): 811 | def setUp(self): 812 | self.B64_TXS = [ 813 | '+Hw8AQGKQklQAAAAAAAAAAKi4YpPTEJJVFgAAAAAiQHluPqP4qwAAIpMRU1PTgAAAAAAgICAAbhF+EMboPtT5w1Brh5BO66qs75e1sOj9Ka4KfxGQDOFsQssgNn/oAeDnTkaMj685tdWvWa6rUmViaCB+KerPBDHUE7O731j', 814 | '+H+DBJaHAQGKQklQAAAAAAAAAAGi4YpCSVAAAAAAAAAAlJXK8ve/qfgqz5QEbX52KCQKOQ/bgICAAbhF+EMcoFVh1TOlkwUHmSpEvOQprRnZgnpSeIFxXn15fApbl28qoFR5GhbH34BiCmpwqY+Qs4xI5DjfE9PNfVYEZ7axgyvc', 815 | '+IeCGxkBAYpCSVAAAAAAAAAAAavqikNPTlNVTEdBTUWUm7rikGga0Jtdjuse5KRJp2SOn8WJAaBVaQ2duAAAgIABuEX4QxygBQd9EAxqcsKigdqsvCEVA5GapLPxdlbZ/DYkC3RXpYOgJ2QuKIW1U/yrTVec56v06V42VwaO2VRqGvuJVLkojvU=', 816 | '+HqCMGYBAYpCSVAAAAAAAAAAA57dikJJUAAAAAAAAACKQ0VOVEFVUlVTAIZa8xB6QACAgAG4RfhDG6A+PinAE4fNMpPwC8U8/DbHNSIERcWDE9rridQ1DyECAaBiGkXNmAgUnYK2VjJjMLJGWLN4T3jz/2KylTFZqXNM5Q==', 817 | '+H6CBYcBAYpCSVAAAAAAAAAAAqLhikJJUAAAAAAAAACJARWORgkT0AAAilRBUFRBUAAAAACAgIABuEX4Qxug8tXe0wJ61FNJq+p/KEVsTE044Cq5mqtnD55xyUM2niigCPjkKq+K5Rabeghgyf7+zxjOhykruz0dQOoZR5GzbZ8=', 818 | '+JKB2AEBikJJUAAAAAAAAAAHt/agd/cYNBCOm15lI3o5JjYxtPmanVhDehOFyTDBPuHU4qaKT05MWTEAAAAAAIm+S9/Lh9P4ZoqAgAG4RfhDG6AVkWCf6F/LQlIUjBFQFCcfAgWxhRBTs7ZdLDLOaUQntaB/C/kHw/fRLDH/ZMM5hdZpx87pZZfTRTuQ6Peda7ParA==', 819 | '+IRVAQGKQklQAAAAAAAAAAGq6YpCSVAAAAAAAAAAlG3tXZ5JAX3uqMMYO7aKsxx9JGTxiIrHIwSJ6AAAgIABuEX4QxugLtA0N8YgapJtAF4/oq4mgnDeTAFy64tAfhlY2POh4fmgTB1pdMhcBzpSiPqUTVPTCn+xwAxpTD8eQsV5+ZWEM40=', 820 | '+JKCTDIBAYpCSVAAAAAAAAAAB7b1oGKbVSjwnRx0qD0YQU8uQmPhSFDEej+sP4VfIAERERERikJJUAAAAAAAAACIRWORgkT0AACAgAG4RfhDG6BbXtr6FKf4Ifdlji3nCSqUQ3OEfReYBUfa71+zNb0wh6AEZSWMY7CGXvh3Rqjjiam9IT3NL7gvI/wrqxSrFkxj/g==', 821 | '+JOCB6IBAYpCSVAAAAAAAAAAB7f2oGKbVSjwnRx0qD0YQU8uQmPhSFDEej+sP4VfIAERERERikJJUAAAAAAAAACJFa8deLWMQAAAgIABuEX4Qxyg3MYUWNZZPy1k4ikM97HJ7RJuLCHyteqUymPVQ+r+qJygcN2yKmUK9adEaqjQRNEHesgRE+dAlamHu2Bd7OE4zwI=', 822 | '+JOCAZIBAYpCSVAAAAAAAAAAB7f2oGKbVSjwnRx0qD0YQU8uQmPhSFDEej+sP4VfIAERERERikJJUAAAAAAAAACJBbEq76+oBAAAgIABuEX4QxygLl0jQ98RtdXCkIKDYce9OD11tpA4kmAtPLXIGkeycAKgGRsYDVVx+SD98N4zKByD2H+w0yToFuZk8gN2PtWVNpQ=', 823 | '+IQJAQGKQklQAAAAAAAAAAGq6YpCSVAAAAAAAAAAlJ9/UFrwbYh8dflHt3kRDT99fUG/iJNP9bP1XNAAgIABuEX4QxugOu3JcCce9OO5rpWDVsQpGk+gwTLbt3p8qu2YM7Tg1UGgF8Ez00im24HKFlJM2DWoUSJ3G1BjzREg1AGG6dP1s1s=', 824 | '+H6CB1UBAYpCSVAAAAAAAAAAAqLhikJJUAAAAAAAAACJEENWGogpMAAAikNFTlRBVVJVUwCAgIABuEX4QxugfLaJkvWttDTqA9EelJ+9RQ8anWwngbIkeOuQydh0RI2gB0JC+oxPrSmB8dZmazeh3ot2Ff6bE2czGfwgPu7ZpCg=', 825 | '+PaCAw8BAYpCSVAAAAAAAAAAAavqikJJUAAAAAAAAACUvW+bnucw5qCqmK4SHaLeJ1gp4YSJYadP9azWWcAAuG5CaXBleC5uZXQgLSBTdWNjZXNzIEJpcCB3aXRoZHJhd2FsICMxMzczOC4gT3VyIGN1cnJlbnQgZXhjaGFuZ2UgcmF0ZXMgZm9yIDEgQklQOiAwLjAxNDckIC8gMC4wMTUzJC4gRm9sbG93IHVzLoABuEX4Qxug7zYujmw36ehtj+scZxn+Zlpj3NrtUp924DwBS9+hkJagQtz8zLrB4jgzLnFtDv1F0t1gKQnpU6PyTYGuicMMRe0=', 826 | '+H+DBJaLAQGKQklQAAAAAAAAAAGi4YpCSVAAAAAAAAAAlJXK8ve/qfgqz5QEbX52KCQKOQ/bgICAAbhF+EMboKGqlP2cC2tqtsnntOBRAg9XNsVBBb5wLBjCPlp5RuBaoDLYDXbDwKQz/ONyYoDV9AUI0C7wCME+T/gGeR1BEdCu', 827 | '+JOCARABAYpCSVAAAAAAAAAAB7f2oEiBrRZ8pftYhjIoQfmS1ortiU/8tYq8CA6K07FW8QRbikJJUAAAAAAAAACJA9NIdC2o2QDmgIABuEX4QxugGdqhQ+dkzUft+Pc5/A0TYUXcZLGDLA8C/5Q2GzNhRoygaAxGtwGHh9VCrOUVsUCvxOyMSZkMqIbC2i7LVKNY6qU=', 828 | '+IQJAQGKQklQAAAAAAAAAAGq6YpCSVAAAAAAAAAAlJ9/UFrwbYh8dflHt3kRDT99fUG/iJb2P7XLwAAAgIABuEX4QxygHiZ64dA9EZbsmjLMtDWPvs/Sn4vLjTeCjAizxgEyFhegG9HDwfYDX249KO/kIVXnOsUUBTA1u6KvsImtRTYwyA0=', 829 | '+H6CB1YBAYpCSVAAAAAAAAAAAqLhikJJUAAAAAAAAACJEENWGogpMAAAik9ORUJJUAAAAACAgIABuEX4Qxug81K8K87BiQpmawv8wBT/8wJA7xexSiStKlH+ijGE+/agCOLB7aqTVi62WHw2BgRBIlYMICc3LOSqsWE6lFku3Ew=', 830 | '+IaCAwQBAYpCSVAAAAAAAAAAAarpikZBTENPTgAAAACUhfwWxlBErQ8ZAvfDq2FnB0UsxeaIDeC2s6dkAACAgAG4RfhDG6C1m77cTSA1qllKv8FOW3/BaIO3lVQtrVclDUd5ay3NpaBPHg/nkVgAfN63rRKBYiNZBb+4sa4tVjSLlL2v6yVB5w==', 831 | '+IMrAQGKQklQAAAAAAAAAAKp6IpCSVAAAAAAAAAAiAr2pNB8jwAAikVDT05BAAAAAACITCl3L4h8OpGAgAG4RfhDG6DX7yY6y9xkV640fWT9EMcIH/WDrPu3Y4hX9zpESaijPKBVP7Z56G2J1urB3QZq9cl7AaYbE7EZeAMWsCsidQGXvg==', 832 | '+IUBAQGKQklQAAAAAAAAAAGr6opCSVAAAAAAAAAAlK/SZxtm+xqSGATRe5jj+eCa+bhmiXPriL12u+CcAICAAbhF+EMboJioZmk/kgs9vAcJcvM8VwmoRuws0s02eMT9yMqNr5s7oBk5m7YSzSIKDjRQYRjyaktm+zMQI4JluYnQVSOpbVkt' 833 | ] 834 | 835 | @staticmethod 836 | def base64ToHex(b64str): 837 | b64_bytes = b64str.encode() 838 | tx_bytes = base64.b64decode(b64_bytes) 839 | 840 | return tx_bytes.hex() 841 | 842 | def test_txs(self): 843 | for index, b64_tx in enumerate(self.B64_TXS): 844 | try: 845 | raw_tx = self.base64ToHex(b64_tx) 846 | MinterTx.from_raw(raw_tx) 847 | except Exception as e: 848 | self.fail(f'Tx #{index} from base64 failed: {e.__str__()}') 849 | 850 | 851 | class TestMinterCreateMultisigTx(unittest.TestCase): 852 | 853 | def setUp(self): 854 | self.FROM = 'Mx3e4d56e776ff42c023b1ec99a7486b592a654981' 855 | self.PRIVATE_KEY = 'bc3503cae8c8561df5eadc4a9eda21d32c252a6c94cfae55b5310bf6085c8582' 856 | self.SIGNED_TX = 'f8a30102018a4d4e54000000000000000cb848f84607c3010305f83f94ee81347211c72524338f9680072af9074433314394ee81347211c72524338f9680072af9074433314594ee81347211c72524338f9680072af90744333144808001b845f8431ca094eb41d39e6782f5539615cc66da7073d4283893f0b3ee2b2f36aee1eaeb7c57a037f90ffdb45eb9b6f4cf301b48e73a6a81df8182e605b656a52057537d264ab4' 857 | self.TX = MinterCreateMultisigTx(**{ 858 | 'nonce': 1, 859 | 'chain_id': MinterTx.TESTNET_CHAIN_ID, 860 | 'gas_coin': 'MNT', 861 | 'threshold': 7, 862 | 'weights': [1, 3, 5], 863 | 'addresses': [ 864 | 'Mxee81347211c72524338f9680072af90744333143', 865 | 'Mxee81347211c72524338f9680072af90744333145', 866 | 'Mxee81347211c72524338f9680072af90744333144' 867 | ] 868 | }) 869 | 870 | def test_valid_tx(self): 871 | """ Is tx instance of needed TX class. """ 872 | self.assertIsInstance(self.TX, MinterCreateMultisigTx) 873 | 874 | with self.assertRaisesRegex(ValueError, 'threshold'): 875 | MinterCreateMultisigTx( 876 | nonce=1, gas_coin='mnt', threshold=1.1, weights=[1], 877 | addresses=['Mxee81347211c72524338f9680072af90744333143'] 878 | ) 879 | 880 | with self.assertRaisesRegex(ValueError, 'weights'): 881 | MinterCreateMultisigTx( 882 | nonce=1, gas_coin='mnt', threshold=1, weights=[0, '1', 1024], 883 | addresses=['Mxee81347211c72524338f9680072af90744333143'] 884 | ) 885 | 886 | def test_sign_tx(self): 887 | """ Sign transaction and check signed transaction """ 888 | self.TX.sign(self.PRIVATE_KEY) 889 | self.assertEqual(self.TX.signed_tx, self.SIGNED_TX) 890 | 891 | def test_from_raw(self): 892 | tx = MinterTx.from_raw(raw_tx=self.SIGNED_TX) 893 | 894 | self.assertEqual(tx.from_mx, self.FROM) 895 | self.assertEqual(tx.threshold, self.TX.threshold) 896 | self.assertEqual(tx.weights, self.TX.weights) 897 | self.assertEqual(tx.addresses, self.TX.addresses) 898 | 899 | 900 | if __name__ == '__main__': 901 | unittest.main() 902 | -------------------------------------------------------------------------------- /mintersdk/test/test_wallet.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mintersdk.sdk.wallet import MinterWallet 4 | 5 | 6 | class TestMinterWallet(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.mnemonic = 'slice better asset talent state citizen dry maze base agent source reveal' 10 | self.private_key = '7ffc6bc08f2d8a0ead1d3f64e6a9862b7695dafceca24f25978341447594aa07' 11 | self.address = 'Mx5a4c6c7fbd05ff8e5b09818db5ad229852784e01' 12 | 13 | def test_private_key(self): 14 | wallet = MinterWallet.create(mnemonic=self.mnemonic) 15 | self.assertEqual(wallet['private_key'], self.private_key) 16 | 17 | def test_address(self): 18 | wallet = MinterWallet.create(mnemonic=self.mnemonic) 19 | self.assertEqual(wallet['address'], self.address) 20 | 21 | def test_creation(self): 22 | for _ in range(250): 23 | MinterWallet.create() 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import codecs 3 | 4 | with codecs.open("README.md", "r", 'utf_8_sig') as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="minter-sdk", 9 | version="1.0.34", 10 | author="U-node Team", 11 | author_email="rymka1989@gmail.com", 12 | description=u"Python SDK for Minter Network", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/U-node/minter-sdk", 16 | packages=setuptools.find_packages(include=['mintersdk']), 17 | include_package_data=True, 18 | classifiers=( 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ), 23 | install_requires=[ 24 | 'rlp', 25 | 'sslcrypto', 26 | 'mnemonic', 27 | 'pysha3', 28 | 'requests', 29 | 'pyqrcode', 30 | 'deprecated' 31 | ] 32 | ) 33 | --------------------------------------------------------------------------------