├── .github └── workflows │ ├── deploypypi.yml │ └── unittests.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── examples.md ├── extensions │ ├── index.md │ ├── polkascan-extension.md │ ├── subsquid-extension.md │ └── substrate-node-extension.md ├── getting-started │ ├── common-concepts.md │ └── installation.md ├── index.md ├── reference │ ├── base.md │ ├── contracts.md │ ├── extensions.md │ ├── interfaces.md │ ├── keypair.md │ └── storage.md └── usage │ ├── call-runtime-apis.md │ ├── cleanup-and-context-manager.md │ ├── extensions.md │ ├── extrinsics.md │ ├── ink-contract-interfacing.md │ ├── keypair-creation-and-signing.md │ ├── query-storage.md │ ├── subscriptions.md │ └── using-scaletype-objects.md ├── examples ├── assets │ ├── flipper-v4.json │ ├── flipper-v4.wasm │ ├── flipper-v5.json │ ├── flipper-v5.wasm │ ├── flipper.json │ └── flipper.wasm ├── balance_transfer.py ├── batch_call.py ├── create_and_exec_contract.py ├── extensions.py ├── fee_info.py ├── historic_balance.py ├── multisig.py ├── start_local_substrate_node.sh └── storage_subscription.py ├── mkdocs.yml ├── requirements.txt ├── setup.py ├── substrateinterface ├── __init__.py ├── base.py ├── constants.py ├── contracts.py ├── exceptions.py ├── extensions.py ├── interfaces.py ├── key.py ├── keypair.py ├── storage.py └── utils │ ├── __init__.py │ ├── caching.py │ ├── ecdsa_helpers.py │ ├── encrypted_json.py │ ├── hasher.py │ └── ss58.py └── test ├── __init__.py ├── fixtures.py ├── fixtures ├── erc20-v0.json ├── erc20-v1.json ├── erc20-v3.json ├── flipper-v3.json ├── flipper-v4.json ├── flipper-v5.json ├── incorrect_metadata.json ├── metadata_hex.json ├── polkadotjs_encrypted.json ├── polkadotjs_encrypted_ed25519.json └── unsupported_type_metadata.json ├── settings.py ├── test_block.py ├── test_contracts.py ├── test_create_extrinsics.py ├── test_extension_interface.py ├── test_helper_functions.py ├── test_init.py ├── test_keypair.py ├── test_query.py ├── test_query_map.py ├── test_rpc_compatibility.py ├── test_runtime_call.py ├── test_ss58.py ├── test_subscriptions.py └── test_type_registry.py /.github/workflows/deploypypi.yml: -------------------------------------------------------------------------------- 1 | name: Test and deploy to PYPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.12' 16 | - name: Install project dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | # stop the build if there are Python syntax errors or undefined names 24 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 26 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 27 | - name: Test with pytest 28 | run: | 29 | pip install pytest 30 | pytest 31 | - name: Install deploy dependencies 32 | run: | 33 | pip install setuptools wheel twine 34 | - name: Build and publish 35 | env: 36 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 37 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 38 | run: | 39 | python setup.py sdist bdist_wheel 40 | twine upload dist/* 41 | -------------------------------------------------------------------------------- /.github/workflows/unittests.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: 4 | push: 5 | branches: [master, develop, v1.0, v0.13] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master, develop, v1.0, v0.13] 9 | schedule: 10 | - cron: '0 7 * * *' 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.12] 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v1 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install -r requirements.txt 31 | - name: Lint with flake8 32 | run: | 33 | pip install flake8 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pip install pytest 41 | pytest 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | 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 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | /.idea 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/polkascan/py-substrate-interface/unittests.yml?branch=master)](https://github.com/JAMdotTech/py-polkadot-sdk/actions?query=workflow%3A%22Run+unit+tests%22) 4 | [![Latest Version](https://img.shields.io/pypi/v/substrate-interface.svg)](https://pypi.org/project/substrate-interface/) 5 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/substrate-interface.svg)](https://pypi.org/project/substrate-interface/) 6 | [![License](https://img.shields.io/pypi/l/substrate-interface.svg)](https://github.com/JAMdotTech/py-polkadot-sdk/blob/master/LICENSE) 7 | 8 | 9 | ## Description 10 | This library specializes in interfacing with a [Substrate](https://substrate.io/) node; querying storage, composing extrinsics, 11 | SCALE encoding/decoding and providing additional convenience methods to deal with the features and metadata of 12 | the Substrate runtime. 13 | 14 | ## Documentation 15 | 16 | * [Library documentation](https://jamdottech.github.io/py-polkadot-sdk/) 17 | * [Metadata documentation for Polkadot and Kusama ecosystem runtimes](https://jamdottech.github.io/py-polkadot-metadata-docs/) 18 | 19 | ## Installation 20 | ```bash 21 | pip install substrate-interface 22 | ``` 23 | 24 | ## Initialization 25 | 26 | ```python 27 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 28 | ``` 29 | 30 | After connecting certain properties like `ss58_format` will be determined automatically by querying the RPC node. At 31 | the moment this will work for most `MetadataV14` and above runtimes like Polkadot, Kusama, Acala, Moonbeam. For 32 | older or runtimes under development the `ss58_format` (default 42) and other properties should be set manually. 33 | 34 | ## Quick usage 35 | 36 | ### Balance information of an account 37 | ```python 38 | result = substrate.query('System', 'Account', ['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T']) 39 | print(result.value['data']['free']) # 635278638077956496 40 | ``` 41 | ### Create balance transfer extrinsic 42 | 43 | ```python 44 | call = substrate.compose_call( 45 | call_module='Balances', 46 | call_function='transfer', 47 | call_params={ 48 | 'dest': '5E9oDs9PjpsBbxXxRE9uMaZZhnBAV38n2ouLB28oecBDdeQo', 49 | 'value': 1 * 10**12 50 | } 51 | ) 52 | 53 | keypair = Keypair.create_from_uri('//Alice') 54 | extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair) 55 | 56 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 57 | 58 | print(f"Extrinsic '{receipt.extrinsic_hash}' sent and included in block '{receipt.block_hash}'") 59 | ``` 60 | 61 | ## Contact and Support 62 | 63 | For questions, please see the [Substrate StackExchange](https://substrate.stackexchange.com/questions/tagged/python) or [Github Discussions](https://github.com/JAMdotTech/py-polkadot-sdk/discussions). 64 | 65 | ## License 66 | https://github.com/JAMdotTech/py-polkadot-sdk/blob/master/LICENSE 67 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Batch call 2 | 3 | ```python 4 | from substrateinterface import SubstrateInterface, Keypair 5 | from substrateinterface.exceptions import SubstrateRequestException 6 | 7 | substrate = SubstrateInterface( 8 | url="ws://127.0.0.1:9944" 9 | ) 10 | 11 | keypair = Keypair.create_from_uri('//Alice') 12 | 13 | balance_call = substrate.compose_call( 14 | call_module='Balances', 15 | call_function='transfer_keep_alive', 16 | call_params={ 17 | 'dest': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 18 | 'value': 1 * 10**15 19 | } 20 | ) 21 | 22 | call = substrate.compose_call( 23 | call_module='Utility', 24 | call_function='batch', 25 | call_params={ 26 | 'calls': [balance_call, balance_call] 27 | } 28 | ) 29 | 30 | extrinsic = substrate.create_signed_extrinsic( 31 | call=call, 32 | keypair=keypair, 33 | era={'period': 64} 34 | ) 35 | 36 | 37 | try: 38 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 39 | 40 | print('Extrinsic "{}" included in block "{}"'.format( 41 | receipt.extrinsic_hash, receipt.block_hash 42 | )) 43 | 44 | if receipt.is_success: 45 | 46 | print('✅ Success, triggered events:') 47 | for event in receipt.triggered_events: 48 | print(f'* {event.value}') 49 | 50 | else: 51 | print('⚠️ Extrinsic Failed: ', receipt.error_message) 52 | 53 | 54 | except SubstrateRequestException as e: 55 | print("Failed to send: {}".format(e)) 56 | ``` 57 | 58 | ## Fee info 59 | 60 | ```python 61 | from substrateinterface import SubstrateInterface, Keypair 62 | 63 | 64 | # import logging 65 | # logging.basicConfig(level=logging.DEBUG) 66 | 67 | 68 | substrate = SubstrateInterface( 69 | url="ws://127.0.0.1:9944" 70 | ) 71 | 72 | keypair = Keypair.create_from_uri('//Alice') 73 | 74 | call = substrate.compose_call( 75 | call_module='Balances', 76 | call_function='transfer_keep_alive', 77 | call_params={ 78 | 'dest': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 79 | 'value': 1 * 10**15 80 | } 81 | ) 82 | 83 | # Get payment info 84 | payment_info = substrate.get_payment_info(call=call, keypair=keypair) 85 | 86 | print("Payment info: ", payment_info) 87 | ``` 88 | 89 | ## Query a Mapped storage function 90 | 91 | ```python 92 | from substrateinterface import SubstrateInterface 93 | 94 | substrate = SubstrateInterface( 95 | url="ws://127.0.0.1:9944" 96 | ) 97 | 98 | result = substrate.query_map("System", "Account", max_results=100) 99 | 100 | for account, account_info in result: 101 | print(f'* {account.value}: {account_info.value}') 102 | ``` 103 | 104 | ## Multisig transaction 105 | 106 | ```python 107 | from substrateinterface import SubstrateInterface, Keypair 108 | 109 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 110 | 111 | keypair_alice = Keypair.create_from_uri('//Alice', ss58_format=substrate.ss58_format) 112 | keypair_bob = Keypair.create_from_uri('//Bob', ss58_format=substrate.ss58_format) 113 | keypair_charlie = Keypair.create_from_uri('//Charlie', ss58_format=substrate.ss58_format) 114 | 115 | # Generate multi-sig account from signatories and threshold 116 | multisig_account = substrate.generate_multisig_account( 117 | signatories=[ 118 | keypair_alice.ss58_address, 119 | keypair_bob.ss58_address, 120 | keypair_charlie.ss58_address 121 | ], 122 | threshold=2 123 | ) 124 | 125 | call = substrate.compose_call( 126 | call_module='Balances', 127 | call_function='transfer_keep_alive', 128 | call_params={ 129 | 'dest': keypair_alice.ss58_address, 130 | 'value': 3 * 10 ** 3 131 | } 132 | ) 133 | 134 | # Initiate multisig tx 135 | extrinsic = substrate.create_multisig_extrinsic(call, keypair_alice, multisig_account, era={'period': 64}) 136 | 137 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 138 | 139 | if not receipt.is_success: 140 | print(f"⚠️ {receipt.error_message}") 141 | exit() 142 | 143 | # Finalize multisig tx with other signatory 144 | extrinsic = substrate.create_multisig_extrinsic(call, keypair_bob, multisig_account, era={'period': 64}) 145 | 146 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 147 | 148 | if receipt.is_success: 149 | print(f"✅ {receipt.triggered_events}") 150 | else: 151 | print(f"⚠️ {receipt.error_message}") 152 | ``` 153 | 154 | ## Create and call ink! contract 155 | 156 | ```python 157 | import os 158 | 159 | from substrateinterface.contracts import ContractCode, ContractInstance 160 | from substrateinterface import SubstrateInterface, Keypair 161 | 162 | substrate = SubstrateInterface( 163 | url="ws://127.0.0.1:9944" 164 | ) 165 | 166 | keypair = Keypair.create_from_uri('//Alice') 167 | contract_address = "5GhwarrVMH8kjb8XyW6zCfURHbHy3v84afzLbADyYYX6H2Kk" 168 | 169 | # Check if contract is on chain 170 | contract_info = substrate.query("Contracts", "ContractInfoOf", [contract_address]) 171 | 172 | if contract_info.value: 173 | 174 | print(f'Found contract on chain: {contract_info.value}') 175 | 176 | # Create contract instance from deterministic address 177 | contract = ContractInstance.create_from_address( 178 | contract_address=contract_address, 179 | metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper.json'), 180 | substrate=substrate 181 | ) 182 | else: 183 | 184 | # Upload WASM code 185 | code = ContractCode.create_from_contract_files( 186 | metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper.json'), 187 | wasm_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper.wasm'), 188 | substrate=substrate 189 | ) 190 | 191 | # Deploy contract 192 | print('Deploy contract...') 193 | contract = code.deploy( 194 | keypair=keypair, 195 | constructor="new", 196 | args={'init_value': True}, 197 | value=0, 198 | gas_limit={'ref_time': 25990000000, 'proof_size': 11990383647911208550}, 199 | upload_code=True 200 | ) 201 | 202 | print(f'✅ Deployed @ {contract.contract_address}') 203 | 204 | # Read current value 205 | result = contract.read(keypair, 'get') 206 | print('Current value of "get":', result.contract_result_data) 207 | 208 | # Do a gas estimation of the message 209 | gas_predit_result = contract.read(keypair, 'flip') 210 | 211 | print('Result of dry-run: ', gas_predit_result.value) 212 | print('Gas estimate: ', gas_predit_result.gas_required) 213 | 214 | # Do the actual call 215 | print('Executing contract call...') 216 | contract_receipt = contract.exec(keypair, 'flip', args={ 217 | 218 | }, gas_limit=gas_predit_result.gas_required) 219 | 220 | if contract_receipt.is_success: 221 | print(f'Events triggered in contract: {contract_receipt.contract_events}') 222 | else: 223 | print(f'Error message: {contract_receipt.error_message}') 224 | 225 | result = contract.read(keypair, 'get') 226 | 227 | print('Current value of "get":', result.contract_result_data) 228 | 229 | ``` 230 | 231 | ## Historic balance 232 | 233 | ```python 234 | from substrateinterface import SubstrateInterface 235 | 236 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 237 | 238 | block_number = 10 239 | block_hash = substrate.get_block_hash(block_number) 240 | 241 | result = substrate.query( 242 | "System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"], block_hash=block_hash 243 | ) 244 | 245 | 246 | def format_balance(amount: int): 247 | amount = format(amount / 10**substrate.properties.get('tokenDecimals', 0), ".15g") 248 | return f"{amount} {substrate.properties.get('tokenSymbol', 'UNIT')}" 249 | 250 | 251 | balance = (result.value["data"]["free"] + result.value["data"]["reserved"]) 252 | 253 | print(f"Balance @ {block_number}: {format_balance(balance)}") 254 | ``` 255 | 256 | ## Block headers subscription 257 | 258 | ```python 259 | from substrateinterface import SubstrateInterface 260 | 261 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 262 | 263 | 264 | def subscription_handler(obj, update_nr, subscription_id): 265 | print(f"New block #{obj['header']['number']}") 266 | 267 | block = substrate.get_block(block_number=obj['header']['number']) 268 | 269 | for idx, extrinsic in enumerate(block['extrinsics']): 270 | print(f'# {idx}: {extrinsic.value}') 271 | 272 | if update_nr > 2: 273 | return {'message': 'Subscription will cancel when a value is returned', 'updates_processed': update_nr} 274 | 275 | 276 | result = substrate.subscribe_block_headers(subscription_handler) 277 | print(result) 278 | ``` 279 | 280 | ## Storage subscription 281 | 282 | ```python 283 | from substrateinterface import SubstrateInterface 284 | 285 | substrate = SubstrateInterface( 286 | url="ws://127.0.0.1:9944" 287 | ) 288 | 289 | 290 | def subscription_handler(account_info_obj, update_nr, subscription_id): 291 | 292 | if update_nr == 0: 293 | print('Initial account data:', account_info_obj.value) 294 | 295 | if update_nr > 0: 296 | # Do something with the update 297 | print('Account data changed:', account_info_obj.value) 298 | 299 | # The execution will block until an arbitrary value is returned, which will be the result of the `query` 300 | if update_nr > 5: 301 | return account_info_obj 302 | 303 | 304 | result = substrate.query("System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"], 305 | subscription_handler=subscription_handler) 306 | 307 | print(result) 308 | ``` 309 | 310 | ## Subscribe to multiple storage keys 311 | 312 | ```python 313 | from substrateinterface import SubstrateInterface 314 | 315 | 316 | def subscription_handler(storage_key, updated_obj, update_nr, subscription_id): 317 | print(f"Update for {storage_key.params[0]}: {updated_obj.value}") 318 | 319 | 320 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 321 | 322 | # Accounts to track 323 | storage_keys = [ 324 | substrate.create_storage_key( 325 | "System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"] 326 | ), 327 | substrate.create_storage_key( 328 | "System", "Account", ["5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"] 329 | ) 330 | ] 331 | 332 | result = substrate.subscribe_storage( 333 | storage_keys=storage_keys, subscription_handler=subscription_handler 334 | ) 335 | ``` 336 | -------------------------------------------------------------------------------- /docs/extensions/index.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | The extension framework is designed to enhance and improve search capabilities on top of existing functionality provided 4 | by the Substrate node. 5 | 6 | It allows for the integration of third-party search indices, which can be easily interchanged with 7 | other data sources that provide the same functionality, as long as they adhere to standardized naming conventions in 8 | the extension registry. 9 | 10 | ## Available extensions 11 | 12 | | Name | Maintained by | Code | 13 | |------------------------------------------------------|----------------------|-----------------------------------------------------------------------------------| 14 | | [SubstrateNodeExtension](./substrate-node-extension) | Polkascan Foundation | [Github](https://github.com/polkascan/py-substrate-interface) | 15 | | [PolkascanExtension](./polkascan-extension.md) | Polkascan Foundation | [Github](https://github.com/polkascan/py-substrate-interface-extension-polkascan) | 16 | | [SubsquidExtension](./subsquid-extension.md) | Polkascan Foundation | [Github](https://github.com/polkascan/py-substrate-interface-extension-subsquid) | 17 | 18 | ## Available extension calls 19 | 20 | | | | 21 | |-----------------|-------------------------------------------------------------------------------------------------------------------------| 22 | | `filter_events` | Filters events to match provided search criteria e.g. block range, pallet name, accountID in attributes | 23 | | `filter_extrinsics` | Filters extrinsics to match provided search criteria e.g. block range, pallet name, signed by accountID | 24 | | `search_block_number` | Search corresponding block number for provided `block_datetime`. the prediction tolerance is provided with `block_time` | 25 | | `get_block_timestamp` | Return a UNIX timestamp for given `block_number`. | 26 | 27 | -------------------------------------------------------------------------------- /docs/extensions/polkascan-extension.md: -------------------------------------------------------------------------------- 1 | # PolkascanExtension 2 | 3 | This extension enables indexes provided by [Polkascan Explorer API](https://github.com/polkascan/explorer#explorer-api-component). 4 | 5 | Maintained by [Polkascan Foundation](https://github.com/polkascan/py-substrate-interface-extension-polkascan). 6 | 7 | ## Installation 8 | ```bash 9 | pip install substrate-interface-polkascan 10 | ``` 11 | 12 | ## Initialization 13 | 14 | ```python 15 | from substrateinterface import SubstrateInterface 16 | from substrateinterface_polkascan.extensions import PolkascanExtension 17 | 18 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 19 | 20 | substrate.register_extension(PolkascanExtension(url='http://127.0.0.1:8000/graphql/')) 21 | ``` 22 | 23 | ## Implemented extension calls 24 | 25 | ### Filter events 26 | 27 | ```python 28 | events = substrate.extensions.filter_events(pallet_name="Balances", event_name="Transfer", page_size=25) 29 | ``` 30 | 31 | ### Filter extrinsics 32 | 33 | ```python 34 | extrinsics = substrate.extensions.filter_extrinsics( 35 | ss58_address="12L9MSmxHY8YvtZKpA7Vpvac2pwf4wrT3gd2Tx78sCctoXSE", 36 | pallet_name="Balances", call_name="transfer_keep_alive", page_size=25 37 | ) 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/extensions/subsquid-extension.md: -------------------------------------------------------------------------------- 1 | # SubsquidExtension 2 | 3 | This extension enables utilisation of [Giant Squid indexes](https://docs.subsquid.io/giant-squid-api/statuses/) provided by [Subsquid](https://subsquid.io) 4 | 5 | Maintained by [Polkascan Foundation](https://github.com/polkascan/py-substrate-interface-extension-subsquid). 6 | 7 | ## Installation 8 | ```bash 9 | pip install substrate-interface-subsquid 10 | ``` 11 | 12 | ## Initialization 13 | 14 | ```python 15 | from substrateinterface import SubstrateInterface 16 | from substrateinterface_subsquid.extensions import SubsquidExtension 17 | 18 | substrate = SubstrateInterface(url="wss://rpc.polkadot.io") 19 | 20 | substrate.register_extension(SubsquidExtension(url='https://squid.subsquid.io/gs-explorer-polkadot/graphql')) 21 | ``` 22 | 23 | ## Implemented extension calls 24 | 25 | ### Filter events 26 | 27 | ```python 28 | events = substrate.extensions.filter_events( 29 | pallet_name="Balances", event_name="Transfer", account_id="12L9MSmxHY8YvtZKpA7Vpvac2pwf4wrT3gd2Tx78sCctoXSE", 30 | page_size=25 31 | ) 32 | ``` 33 | 34 | ### Filter extrinsics 35 | 36 | ```python 37 | extrinsics = substrate.extensions.filter_extrinsics( 38 | ss58_address="12L9MSmxHY8YvtZKpA7Vpvac2pwf4wrT3gd2Tx78sCctoXSE", 39 | pallet_name="Balances", call_name="transfer_keep_alive", page_size=25 40 | ) 41 | ``` 42 | 43 | ### Search block number 44 | 45 | ```python 46 | block_datetime = datetime(2020, 7, 12, 0, 0, 0, tzinfo=timezone.utc) 47 | 48 | block_number = substrate.extensions.search_block_number(block_datetime=block_datetime) 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/extensions/substrate-node-extension.md: -------------------------------------------------------------------------------- 1 | # SubstrateNodeExtension 2 | 3 | This extensions is meant as a fallback option that uses only existing Substrate RPC methods. 4 | However, it is important to note that this fallback implementation is significantly inefficient, and it is encouraged to utilize third-party search indices where possible for optimal search performance. 5 | 6 | ## Initialization 7 | 8 | ```python 9 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 10 | # Provide maximum block range (bigger range descreases performance) 11 | substrate.register_extension(SubstrateNodeExtension(max_block_range=100)) 12 | ``` 13 | 14 | ## Implemented extension calls 15 | 16 | ### filter_events 17 | ```python 18 | # Returns all `Balances.Transfer` events from the last 30 blocks 19 | events = substrate.extensions.filter_events(pallet_name="Balances", event_name="Transfer", block_start=-30) 20 | ``` 21 | 22 | ### filter_extrinsics 23 | 24 | ```python 25 | # All Timestamp extrinsics in block range #3 until #6 26 | extrinsics = substrate.extensions.filter_extrinsics(pallet_name="Timestamp", block_start=3, block_end=6) 27 | ``` 28 | 29 | ### search_block_number 30 | 31 | ```python 32 | # Search for block number corresponding a specific datetime 33 | block_datetime = datetime(2020, 7, 12, 0, 0, 0) 34 | 35 | block_number = substrate.extensions.search_block_number(block_datetime=block_datetime) 36 | ``` 37 | 38 | ### get_block_timestamp 39 | 40 | ```python 41 | # Get timestamp for specific block number 42 | block_timestamp = substrate.extensions.get_block_timestamp(block_number) 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/getting-started/common-concepts.md: -------------------------------------------------------------------------------- 1 | 2 | ## SCALE 3 | Substrate uses a lightweight and efficient 4 | [encoding and decoding program](https://docs.substrate.io/reference/scale-codec/) to optimize how data is sent and 5 | received over the network. The program used to serialize and deserialize data is called the SCALE codec, with SCALE 6 | being an acronym for **S**imple **C**oncatenated **A**ggregate **L**ittle-**E**ndian. 7 | 8 | This library utilizes [py-scale-codec](https://github.com/polkascan/py-scale-codec) for encoding and decoding SCALE, see 9 | [this overview](https://polkascan.github.io/py-scale-codec/#examples-of-different-types) for more information how 10 | to encode data from Python. 11 | 12 | ## SS58 address formatting 13 | 14 | SS58 is a simple address format designed for Substrate based chains. For more information about its specification 15 | see the [Substrate documentation about SS58](https://docs.substrate.io/reference/address-formats/) 16 | 17 | ## Extrinsics 18 | 19 | Extrinsics within Substrate are basically signed transactions, a vehicle to execute a call function within the 20 | Substrate runtime, originated from outside the runtime. More information about extrinsics 21 | on [Substrate docs](https://docs.substrate.io/reference/transaction-format/). For more information on which call 22 | functions are available in existing Substrate implementations, refer to 23 | the [PySubstrate Metadata Docs](https://polkascan.github.io/py-substrate-metadata-docs/) 24 | 25 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | ## Install using PyPI 2 | ```bash 3 | pip install substrate-interface 4 | ``` 5 | 6 | ## Initialization 7 | 8 | ```python 9 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 10 | ``` 11 | 12 | After connecting certain properties like `ss58_format` will be determined automatically by querying the RPC node. At 13 | the moment this will work for most `MetadataV14` and above runtimes like Polkadot, Kusama, Acala, Moonbeam. For 14 | older or runtimes under development the `ss58_format` (default 42) and other properties should be set manually. 15 | 16 | ## Quick usage 17 | 18 | ### Balance information of an account 19 | ```python 20 | result = substrate.query('System', 'Account', ['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T']) 21 | print(result.value['data']['free']) # 635278638077956496 22 | ``` 23 | ### Create balance transfer extrinsic 24 | 25 | ```python 26 | call = substrate.compose_call( 27 | call_module='Balances', 28 | call_function='transfer_keep_alive', 29 | call_params={ 30 | 'dest': '5E9oDs9PjpsBbxXxRE9uMaZZhnBAV38n2ouLB28oecBDdeQo', 31 | 'value': 1 * 10**12 32 | } 33 | ) 34 | keypair = Keypair.create_from_uri('//Alice') 35 | extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair) 36 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 37 | 38 | print(f"Extrinsic '{receipt.extrinsic_hash}' sent and included in block '{receipt.block_hash}'") 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | This library specializes in interfacing with a [Substrate](https://substrate.io) node; querying storage, 2 | composing extrinsics, [SCALE](getting-started/common-concepts/#scale) encoding/decoding and providing additional convenience methods 3 | to deal with the features and metadata of the Substrate runtime. 4 | 5 | ## Getting started 6 | About [installation, initialization](getting-started/installation/) and useful background information. 7 | 8 | ## Usage 9 | [Overview of available functionality](usage/query-storage/) and how to use it. 10 | 11 | ## Function Reference 12 | [Extensive reference](reference/base/) of functions and classes in the library. 13 | 14 | ## Examples 15 | [Various code snippets](examples.md) for common use-cases. 16 | 17 | ## Extensions 18 | Overview of available [extensions](/extensions/); adding or improving existing functionality. 19 | 20 | ## Metadata docs 21 | [Documentation of Substrate metadata](https://polkascan.github.io/py-substrate-metadata-docs/) for well known runtimes and how to use it with py-substrate-interface. 22 | 23 | ## Contact and Support 24 | 25 | For questions, please see the [Substrate StackExchange](https://substrate.stackexchange.com/questions/tagged/python), [Github Discussions](https://github.com/polkascan/py-substrate-interface/discussions) or 26 | reach out to us on our [matrix](http://matrix.org) chat group: [Polkascan Technical](https://matrix.to/#/#polkascan:matrix.org). 27 | 28 | ## License 29 | [https://github.com/polkascan/py-substrate-interface/blob/master/LICENSE](https://github.com/polkascan/py-substrate-interface/blob/master/LICENSE) 30 | -------------------------------------------------------------------------------- /docs/reference/base.md: -------------------------------------------------------------------------------- 1 | ::: substrateinterface.base 2 | -------------------------------------------------------------------------------- /docs/reference/contracts.md: -------------------------------------------------------------------------------- 1 | ::: substrateinterface.contracts 2 | -------------------------------------------------------------------------------- /docs/reference/extensions.md: -------------------------------------------------------------------------------- 1 | ::: substrateinterface.extensions 2 | -------------------------------------------------------------------------------- /docs/reference/interfaces.md: -------------------------------------------------------------------------------- 1 | ::: substrateinterface.interfaces 2 | -------------------------------------------------------------------------------- /docs/reference/keypair.md: -------------------------------------------------------------------------------- 1 | ::: substrateinterface.keypair 2 | -------------------------------------------------------------------------------- /docs/reference/storage.md: -------------------------------------------------------------------------------- 1 | ::: substrateinterface.storage 2 | -------------------------------------------------------------------------------- /docs/usage/call-runtime-apis.md: -------------------------------------------------------------------------------- 1 | # Call runtime APIs 2 | 3 | Each Substrate node contains a runtime. The runtime contains the business logic of the chain. It defines what 4 | transactions are valid and invalid and determines how the chain's state changes in response to transactions. 5 | 6 | A Runtime API facilitates this kind of communication between the outer node and the runtime. 7 | [More information about Runtime APIs](https://docs.substrate.io/reference/runtime-apis/) 8 | 9 | ## Example 10 | ```python 11 | result = substrate.runtime_call("AccountNonceApi", "account_nonce", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"]) 12 | # 13 | ``` 14 | 15 | ## List of available runtime APIs and methods 16 | 17 | ```python 18 | runtime_calls = substrate.get_metadata_runtime_call_functions() 19 | #[ 20 | # 21 | # ... 22 | #] 23 | ``` 24 | 25 | ## Get param type decomposition 26 | A helper function to compose the parameters for this runtime API call 27 | 28 | ```python 29 | runtime_call = substrate.get_metadata_runtime_call_function("ContractsApi", "call") 30 | param_info = runtime_call.get_param_info() 31 | # ['AccountId', 'AccountId', 'u128', 'u64', (None, 'u128'), 'Bytes'] 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/usage/cleanup-and-context-manager.md: -------------------------------------------------------------------------------- 1 | # Cleanup and context manager 2 | 3 | At the end of the lifecycle of a `SubstrateInterface` instance, calling the `close()` method will do all the necessary 4 | cleanup, like closing the websocket connection. 5 | 6 | When using the context manager this will be done automatically: 7 | 8 | ```python 9 | with SubstrateInterface(url="wss://rpc.polkadot.io") as substrate: 10 | events = substrate.query("System", "Events") 11 | 12 | # connection is now closed 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/usage/extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | Moved to main section "Extensions" 4 | -------------------------------------------------------------------------------- /docs/usage/ink-contract-interfacing.md: -------------------------------------------------------------------------------- 1 | # ink! contract interfacing 2 | ink! is a programming language for smart contracts; blockchains built with the Substrate framework can choose from a 3 | number of smart contract languages which one(s) they want to support. ink! is one of them. It is an opinionated 4 | language that we have built by extending the popular Rust programming language with functionality needed to make 5 | it smart contract compatible. 6 | 7 | [More information about ink!](https://use.ink/) 8 | 9 | ## Deploy a contract 10 | 11 | Tested on [substrate-contracts-node](https://github.com/paritytech/substrate-contracts-node) with the [Flipper contract from the tutorial](https://docs.substrate.io/tutorials/smart-contracts/prepare-your-first-contract/): 12 | 13 | ```python 14 | substrate = SubstrateInterface( 15 | url="ws://127.0.0.1:9944", 16 | type_registry_preset='canvas' 17 | ) 18 | 19 | keypair = Keypair.create_from_uri('//Alice') 20 | 21 | # Deploy contract 22 | code = ContractCode.create_from_contract_files( 23 | metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper.json'), 24 | wasm_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper.wasm'), 25 | substrate=substrate 26 | ) 27 | 28 | contract = code.deploy( 29 | keypair=keypair, 30 | endowment=0, 31 | gas_limit=1000000000000, 32 | constructor="new", 33 | args={'init_value': True}, 34 | upload_code=True 35 | ) 36 | 37 | print(f'✅ Deployed @ {contract.contract_address}') 38 | ``` 39 | 40 | ## Work with an existing instance: 41 | 42 | ```python 43 | # Create contract instance from deterministic address 44 | contract = ContractInstance.create_from_address( 45 | contract_address=contract_address, 46 | metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper.json'), 47 | substrate=substrate 48 | ) 49 | ``` 50 | 51 | ## Read data from a contract: 52 | 53 | ```python 54 | result = contract.read(keypair, 'get') 55 | print('Current value of "get":', result.contract_result_data) 56 | ``` 57 | 58 | ## Execute a contract call 59 | 60 | ```python 61 | # Do a gas estimation of the message 62 | gas_predit_result = contract.read(keypair, 'flip') 63 | 64 | print('Result of dry-run: ', gas_predit_result.value) 65 | print('Gas estimate: ', gas_predit_result.gas_required) 66 | 67 | # Do the actual call 68 | print('Executing contract call...') 69 | contract_receipt = contract.exec(keypair, 'flip', args={ 70 | 71 | }, gas_limit=gas_predit_result.gas_required) 72 | 73 | if contract_receipt.is_success: 74 | print(f'Events triggered in contract: {contract_receipt.contract_events}') 75 | else: 76 | print(f'Error message: {contract_receipt.error_message}') 77 | ``` 78 | 79 | See complete [code example](https://github.com/polkascan/py-substrate-interface/blob/master/examples/create_and_exec_contract.py) for more details 80 | -------------------------------------------------------------------------------- /docs/usage/keypair-creation-and-signing.md: -------------------------------------------------------------------------------- 1 | # Keypair creation and signing 2 | 3 | Keypairs are used to sign transactions and encrypt/decrypt messages. They consist of a public/private key and can be 4 | generated in several ways like by a BIP39 mnemonic: 5 | 6 | ```python 7 | mnemonic = Keypair.generate_mnemonic() 8 | keypair = Keypair.create_from_mnemonic(mnemonic) 9 | signature = keypair.sign("Test123") 10 | if keypair.verify("Test123", signature): 11 | print('Verified') 12 | ``` 13 | 14 | By default, a keypair is using [SR25519](https://research.web3.foundation/en/latest/polkadot/keys/1-accounts-more.html) 15 | cryptography, alternatively ED25519 and ECDSA (for Ethereum-style addresses) can be explicitly specified: 16 | 17 | ```python 18 | keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ECDSA) 19 | print(keypair.ss58_address) 20 | # '0x6741864968e8b87c6e32e19cde88A11a3Cc636E9' 21 | ``` 22 | 23 | ## Creating keypairs with soft and hard key derivation paths 24 | 25 | ```python 26 | mnemonic = Keypair.generate_mnemonic() 27 | keypair = Keypair.create_from_uri(mnemonic + '//hard/soft') 28 | ``` 29 | 30 | By omitting the mnemonic the default development mnemonic is used: 31 | 32 | ```python 33 | keypair = Keypair.create_from_uri('//Alice') 34 | ``` 35 | 36 | ## Creating ECDSA keypairs with BIP44 derivation paths 37 | 38 | ```python 39 | mnemonic = Keypair.generate_mnemonic() 40 | keypair = Keypair.create_from_uri(f"{mnemonic}/m/44'/60'/0'/0/0", crypto_type=KeypairType.ECDSA) 41 | ``` 42 | 43 | ## Create Keypair from PolkadotJS JSON format 44 | 45 | ```python 46 | with open('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY.json', 'r') as fp: 47 | json_data = fp.read() 48 | keypair = Keypair.create_from_encrypted_json(json_data, passphrase="test", ss58_format=42) 49 | ``` 50 | 51 | ## Verify generated signature with public address 52 | 53 | _Example: Substrate style addresses_ 54 | ```python 55 | keypair = Keypair.create_from_uri("//Alice", crypto_type=KeypairType.SR25519) 56 | signature = keypair.sign('test') 57 | 58 | keypair_public = Keypair(ss58_address='5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', crypto_type=KeypairType.SR25519) 59 | result = keypair_public.verify('test', signature) 60 | ``` 61 | 62 | _Example: Ethereum style addresses_ 63 | ```python 64 | keypair = Keypair.create_from_uri("/m/44'/60/0'/0", crypto_type=KeypairType.ECDSA) 65 | signature = keypair.sign('test') 66 | 67 | keypair_public = Keypair(public_key='0x5e20a619338338772e97aa444e001043da96a43b', crypto_type=KeypairType.ECDSA) 68 | result = keypair_public.verify('test', signature) 69 | ``` 70 | 71 | ## Offline signing of extrinsics 72 | 73 | This example generates a signature payload which can be signed on another (offline) machine and later on sent to the 74 | network with the generated signature. 75 | 76 | 1. Generate signature payload on online machine: 77 | ```python 78 | substrate = SubstrateInterface( 79 | url="ws://127.0.0.1:9944", 80 | ss58_format=42, 81 | type_registry_preset='substrate-node-template', 82 | ) 83 | 84 | call = substrate.compose_call( 85 | call_module='Balances', 86 | call_function='transfer_keep_alive', 87 | call_params={ 88 | 'dest': '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 89 | 'value': 2 * 10**8 90 | } 91 | ) 92 | 93 | era = {'period': 64, 'current': 22719} 94 | nonce = 0 95 | 96 | signature_payload = substrate.generate_signature_payload(call=call, era=era, nonce=nonce) 97 | ``` 98 | 99 | 2. Then on another (offline) machine generate the signature with given `signature_payload`: 100 | 101 | ```python 102 | keypair = Keypair.create_from_mnemonic("nature exchange gasp toy result bacon coin broccoli rule oyster believe lyrics") 103 | signature = keypair.sign(signature_payload) 104 | ``` 105 | 106 | 3. Finally on the online machine send the extrinsic with generated signature: 107 | 108 | ```python 109 | keypair = Keypair(ss58_address="5EChUec3ZQhUvY1g52ZbfBVkqjUY9Kcr6mcEvQMbmd38shQL") 110 | 111 | extrinsic = substrate.create_signed_extrinsic( 112 | call=call, 113 | keypair=keypair, 114 | era=era, 115 | nonce=nonce, 116 | signature=signature 117 | ) 118 | 119 | result = substrate.submit_extrinsic( 120 | extrinsic=extrinsic 121 | ) 122 | 123 | print(result.extrinsic_hash) 124 | ``` 125 | -------------------------------------------------------------------------------- /docs/usage/query-storage.md: -------------------------------------------------------------------------------- 1 | # Query storage 2 | 3 | In Substrate, any pallet can introduce new storage items that will become part of the blockchain state. These storage 4 | items can be simple single values, or more complex storage maps. 5 | 6 | The runtime exposes several storage functions to query those storage items and are provided in the metadata. 7 | See the [metadata documentation](https://polkascan.github.io/py-substrate-metadata-docs/) for more information of available storage functions for several Substrate runtimes. 8 | 9 | ## Example 10 | 11 | ```python 12 | result = substrate.query('System', 'Account', ['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T']) 13 | 14 | print(result.value['nonce']) # 7695 15 | print(result.value['data']['free']) # 635278638077956496 16 | ``` 17 | 18 | ## State at a specific block hash 19 | 20 | ```python 21 | account_info = substrate.query( 22 | module='System', 23 | storage_function='Account', 24 | params=['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T'], 25 | block_hash='0x176e064454388fd78941a0bace38db424e71db9d5d5ed0272ead7003a02234fa' 26 | ) 27 | 28 | print(account_info['nonce'].value) # 7673 29 | print(account_info['data']['free'].value) # 637747267365404068 30 | ``` 31 | 32 | ## Type decomposition information 33 | 34 | Some storage functions need parameters and some of those parameter types can be quite complex to compose. 35 | 36 | To retrieve more information how to format those storage function parameters, the helper function `get_param_info()` is available: 37 | 38 | ```python 39 | storage_function = substrate.get_metadata_storage_function("Tokens", "TotalIssuance") 40 | 41 | print(storage_function.get_param_info()) 42 | # [{ 43 | # 'Token': ('ACA', 'AUSD', 'DOT', 'LDOT', 'RENBTC', 'CASH', 'KAR', 'KUSD', 'KSM', 'LKSM', 'TAI', 'BNC', 'VSKSM', 'PHA', 'KINT', 'KBTC'), 44 | # 'DexShare': ({'Token': ('ACA', 'AUSD', 'DOT', 'LDOT', 'RENBTC', 'CASH', 'KAR', 'KUSD', 'KSM', 'LKSM', 'TAI', 'BNC', 'VSKSM', 'PHA', 'KINT', 'KBTC'), 'Erc20': '[u8; 20]', 'LiquidCrowdloan': 'u32', 'ForeignAsset': 'u16'}, {'Token': ('ACA', 'AUSD', 'DOT', 'LDOT', 'RENBTC', 'CASH', 'KAR', 'KUSD', 'KSM', 'LKSM', 'TAI', 'BNC', 'VSKSM', 'PHA', 'KINT', 'KBTC'), 'Erc20': '[u8; 20]', 'LiquidCrowdloan': 'u32', 'ForeignAsset': 'u16'}), 45 | # 'Erc20': '[u8; 20]', 46 | # 'StableAssetPoolToken': 'u32', 47 | # 'LiquidCrowdloan': 'u32', 48 | # 'ForeignAsset': 'u16' 49 | # }] 50 | ``` 51 | 52 | ## Querying multiple storage entries at once 53 | 54 | When a large amount of storage entries is requested, the most efficient way is to use the `query_multi()` function. 55 | This will batch all the requested storage entries in one RPC request. 56 | 57 | ```python 58 | storage_keys = [ 59 | substrate.create_storage_key( 60 | "System", "Account", ["F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T"] 61 | ), 62 | substrate.create_storage_key( 63 | "System", "Account", ["GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi"] 64 | ), 65 | substrate.create_storage_key( 66 | "Staking", "Bonded", ["GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi"] 67 | ) 68 | ] 69 | 70 | result = substrate.query_multi(storage_keys) 71 | 72 | for storage_key, value_obj in result: 73 | print(storage_key, value_obj) 74 | ``` 75 | 76 | ## Query a mapped storage function 77 | Mapped storage functions can be iterated over all key/value pairs, for these type of storage functions `query_map()` 78 | can be used. 79 | 80 | The result is a `QueryMapResult` object, which is an iterator: 81 | 82 | ```python 83 | # Retrieve the first 199 System.Account entries 84 | result = substrate.query_map('System', 'Account', max_results=199) 85 | 86 | for account, account_info in result: 87 | print(f"Free balance of account '{account.value}': {account_info.value['data']['free']}") 88 | ``` 89 | 90 | These results are transparently retrieved in batches capped by the `page_size` kwarg, currently the 91 | maximum `page_size` restricted by the RPC node is 1000 92 | 93 | ```python 94 | # Retrieve all System.Account entries in batches of 200 (automatically appended by `QueryMapResult` iterator) 95 | result = substrate.query_map('System', 'Account', page_size=200, max_results=400) 96 | 97 | for account, account_info in result: 98 | print(f"Free balance of account '{account.value}': {account_info.value['data']['free']}") 99 | ``` 100 | 101 | Querying a `DoubleMap` storage function: 102 | 103 | ```python 104 | era_stakers = substrate.query_map( 105 | module='Staking', 106 | storage_function='ErasStakers', 107 | params=[2100] 108 | ) 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/usage/subscriptions.md: -------------------------------------------------------------------------------- 1 | # Subscriptions 2 | 3 | It is possible to create subscriptions for certain data to get updates pushed as they happen. These subscriptions are 4 | blocking until the subscription is closed. 5 | 6 | ## Storage subscriptions 7 | 8 | When a callable is passed as kwarg `subscription_handler` in the `query()` function, there will be a subscription 9 | created for given storage query. Updates will be pushed to the callable and will block execution until a final value 10 | is returned. This value will be returned as a result of the query and finally automatically unsubscribed from further 11 | updates. 12 | 13 | ```python 14 | def subscription_handler(account_info_obj, update_nr, subscription_id): 15 | 16 | if update_nr == 0: 17 | print('Initial account data:', account_info_obj.value) 18 | 19 | if update_nr > 0: 20 | # Do something with the update 21 | print('Account data changed:', account_info_obj.value) 22 | 23 | # The execution will block until an arbitrary value is returned, which will be the result of the `query` 24 | if update_nr > 5: 25 | return account_info_obj 26 | 27 | 28 | result = substrate.query("System", "Account", ["5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY"], 29 | subscription_handler=subscription_handler) 30 | 31 | print(result) 32 | ``` 33 | 34 | ## Subscribe to multiple storage keys 35 | 36 | To subscribe to multiple storage keys at once, the function `subscribe_storage()` provides the most efficient method. 37 | This will track changes for multiple state entries (storage keys) in just one RPC call to the Substrate node. 38 | 39 | Same as for `query()`, updates will be pushed to the `subscription_handler` callable and will block execution until 40 | a final value is returned. This value will be returned as a result of subscription and finally automatically 41 | unsubscribed from further updates. 42 | 43 | ```python 44 | def subscription_handler(storage_key, updated_obj, update_nr, subscription_id): 45 | print(f"Update for {storage_key.params[0]}: {updated_obj.value}") 46 | 47 | # Accounts to track 48 | storage_keys = [ 49 | substrate.create_storage_key( 50 | "System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"] 51 | ), 52 | substrate.create_storage_key( 53 | "System", "Account", ["5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"] 54 | ) 55 | ] 56 | 57 | result = substrate.subscribe_storage( 58 | storage_keys=storage_keys, subscription_handler=subscription_handler 59 | ) 60 | ``` 61 | 62 | ## Subscribe to new block headers 63 | 64 | ```python 65 | def subscription_handler(obj, update_nr, subscription_id): 66 | print(f"New block #{obj['header']['number']}") 67 | 68 | block = substrate.get_block(block_number=obj['header']['number']) 69 | 70 | for idx, extrinsic in enumerate(block['extrinsics']): 71 | print(f'# {idx}: {extrinsic.value}') 72 | 73 | if update_nr > 10: 74 | return {'message': 'Subscription will cancel when a value is returned', 'updates_processed': update_nr} 75 | 76 | 77 | result = substrate.subscribe_block_headers(subscription_handler) 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/usage/using-scaletype-objects.md: -------------------------------------------------------------------------------- 1 | # Using ScaleType objects 2 | 3 | The result of the previous storage query example is a `ScaleType` object, more specific a `Struct`. 4 | 5 | The nested object structure of this `account_info` object is as follows: 6 | ``` 7 | account_info = , 'consumers': , 'providers': , 'sufficients': , 'data': })> 8 | ``` 9 | 10 | Every `ScaleType` have the following characteristics: 11 | 12 | ## Shorthand lookup of nested types 13 | 14 | Inside the `AccountInfo` struct there are several `U32` objects that represents for example a nonce or the amount of provider, 15 | also another struct object `AccountData` which contains more nested types. 16 | 17 | 18 | To access these nested structures you can access those formally using: 19 | 20 | `account_info.value_object['data'].value_object['free']` 21 | 22 | As a convenient shorthand you can also use: 23 | 24 | `account_info['data']['free']` 25 | 26 | `ScaleType` objects can also be automatically converted to an iterable, so if the object 27 | is for example the `others` in the result Struct of `Staking.eraStakers` can be iterated via: 28 | 29 | ```python 30 | for other_info in era_stakers['others']: 31 | print(other_info['who'], other_info['value']) 32 | ``` 33 | 34 | ## Serializable 35 | Each `ScaleType` holds a complete serialized version of itself in the `account_info.serialize()` property, so it can easily store or used to create JSON strings. 36 | 37 | So the whole result of `account_info.serialize()` will be a `dict` containing the following: 38 | 39 | ```json 40 | { 41 | "nonce": 5, 42 | "consumers": 0, 43 | "providers": 1, 44 | "sufficients": 0, 45 | "data": { 46 | "free": 1152921503981846391, 47 | "reserved": 0, 48 | "misc_frozen": 0, 49 | "fee_frozen": 0 50 | } 51 | } 52 | ``` 53 | 54 | ## Comparing values with `ScaleType` objects 55 | 56 | It is possible to compare ScaleType objects directly to Python primitives, internally the serialized `value` attribute 57 | is compared: 58 | 59 | ```python 60 | metadata_obj[1][1]['extrinsic']['version'] # '' 61 | metadata_obj[1][1]['extrinsic']['version'] == 4 # True 62 | ``` 63 | -------------------------------------------------------------------------------- /examples/assets/flipper-v4.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0x7a145824196b34be760df85e403aabfc81643164514cd7ac79134e30462026c4", 4 | "language": "ink! 4.2.1", 5 | "compiler": "rustc 1.69.0-nightly", 6 | "build_info": { 7 | "build_mode": "Debug", 8 | "cargo_contract_version": "3.0.1", 9 | "rust_toolchain": "nightly-aarch64-apple-darwin", 10 | "wasm_opt_settings": { 11 | "keep_debug_symbols": false, 12 | "optimization_passes": "Z" 13 | } 14 | } 15 | }, 16 | "contract": { 17 | "name": "flippernew", 18 | "version": "0.1.0", 19 | "authors": [ 20 | "[your_name] <[your_email]>" 21 | ] 22 | }, 23 | "spec": { 24 | "constructors": [ 25 | { 26 | "args": [ 27 | { 28 | "label": "init_value", 29 | "type": { 30 | "displayName": [ 31 | "bool" 32 | ], 33 | "type": 0 34 | } 35 | } 36 | ], 37 | "default": false, 38 | "docs": [ 39 | "Constructor that initializes the `bool` value to the given `init_value`." 40 | ], 41 | "label": "new", 42 | "payable": false, 43 | "returnType": { 44 | "displayName": [ 45 | "ink_primitives", 46 | "ConstructorResult" 47 | ], 48 | "type": 1 49 | }, 50 | "selector": "0x9bae9d5e" 51 | }, 52 | { 53 | "args": [], 54 | "default": false, 55 | "docs": [ 56 | "Constructor that initializes the `bool` value to `false`.", 57 | "", 58 | "Constructors can delegate to other constructors." 59 | ], 60 | "label": "default", 61 | "payable": false, 62 | "returnType": { 63 | "displayName": [ 64 | "ink_primitives", 65 | "ConstructorResult" 66 | ], 67 | "type": 1 68 | }, 69 | "selector": "0xed4b9d1b" 70 | } 71 | ], 72 | "docs": [], 73 | "environment": { 74 | "accountId": { 75 | "displayName": [ 76 | "AccountId" 77 | ], 78 | "type": 5 79 | }, 80 | "balance": { 81 | "displayName": [ 82 | "Balance" 83 | ], 84 | "type": 8 85 | }, 86 | "blockNumber": { 87 | "displayName": [ 88 | "BlockNumber" 89 | ], 90 | "type": 11 91 | }, 92 | "chainExtension": { 93 | "displayName": [ 94 | "ChainExtension" 95 | ], 96 | "type": 12 97 | }, 98 | "hash": { 99 | "displayName": [ 100 | "Hash" 101 | ], 102 | "type": 9 103 | }, 104 | "maxEventTopics": 4, 105 | "timestamp": { 106 | "displayName": [ 107 | "Timestamp" 108 | ], 109 | "type": 10 110 | } 111 | }, 112 | "events": [], 113 | "lang_error": { 114 | "displayName": [ 115 | "ink", 116 | "LangError" 117 | ], 118 | "type": 3 119 | }, 120 | "messages": [ 121 | { 122 | "args": [], 123 | "default": false, 124 | "docs": [ 125 | " A message that can be called on instantiated contracts.", 126 | " This one flips the value of the stored `bool` from `true`", 127 | " to `false` and vice versa." 128 | ], 129 | "label": "flip", 130 | "mutates": true, 131 | "payable": false, 132 | "returnType": { 133 | "displayName": [ 134 | "ink", 135 | "MessageResult" 136 | ], 137 | "type": 1 138 | }, 139 | "selector": "0x633aa551" 140 | }, 141 | { 142 | "args": [], 143 | "default": false, 144 | "docs": [ 145 | " Simply returns the current value of our `bool`." 146 | ], 147 | "label": "get", 148 | "mutates": false, 149 | "payable": false, 150 | "returnType": { 151 | "displayName": [ 152 | "ink", 153 | "MessageResult" 154 | ], 155 | "type": 4 156 | }, 157 | "selector": "0x2f865bd9" 158 | } 159 | ] 160 | }, 161 | "storage": { 162 | "root": { 163 | "layout": { 164 | "struct": { 165 | "fields": [ 166 | { 167 | "layout": { 168 | "leaf": { 169 | "key": "0x00000000", 170 | "ty": 0 171 | } 172 | }, 173 | "name": "value" 174 | } 175 | ], 176 | "name": "Flippernew" 177 | } 178 | }, 179 | "root_key": "0x00000000" 180 | } 181 | }, 182 | "types": [ 183 | { 184 | "id": 0, 185 | "type": { 186 | "def": { 187 | "primitive": "bool" 188 | } 189 | } 190 | }, 191 | { 192 | "id": 1, 193 | "type": { 194 | "def": { 195 | "variant": { 196 | "variants": [ 197 | { 198 | "fields": [ 199 | { 200 | "type": 2 201 | } 202 | ], 203 | "index": 0, 204 | "name": "Ok" 205 | }, 206 | { 207 | "fields": [ 208 | { 209 | "type": 3 210 | } 211 | ], 212 | "index": 1, 213 | "name": "Err" 214 | } 215 | ] 216 | } 217 | }, 218 | "params": [ 219 | { 220 | "name": "T", 221 | "type": 2 222 | }, 223 | { 224 | "name": "E", 225 | "type": 3 226 | } 227 | ], 228 | "path": [ 229 | "Result" 230 | ] 231 | } 232 | }, 233 | { 234 | "id": 2, 235 | "type": { 236 | "def": { 237 | "tuple": [] 238 | } 239 | } 240 | }, 241 | { 242 | "id": 3, 243 | "type": { 244 | "def": { 245 | "variant": { 246 | "variants": [ 247 | { 248 | "index": 1, 249 | "name": "CouldNotReadInput" 250 | } 251 | ] 252 | } 253 | }, 254 | "path": [ 255 | "ink_primitives", 256 | "LangError" 257 | ] 258 | } 259 | }, 260 | { 261 | "id": 4, 262 | "type": { 263 | "def": { 264 | "variant": { 265 | "variants": [ 266 | { 267 | "fields": [ 268 | { 269 | "type": 0 270 | } 271 | ], 272 | "index": 0, 273 | "name": "Ok" 274 | }, 275 | { 276 | "fields": [ 277 | { 278 | "type": 3 279 | } 280 | ], 281 | "index": 1, 282 | "name": "Err" 283 | } 284 | ] 285 | } 286 | }, 287 | "params": [ 288 | { 289 | "name": "T", 290 | "type": 0 291 | }, 292 | { 293 | "name": "E", 294 | "type": 3 295 | } 296 | ], 297 | "path": [ 298 | "Result" 299 | ] 300 | } 301 | }, 302 | { 303 | "id": 5, 304 | "type": { 305 | "def": { 306 | "composite": { 307 | "fields": [ 308 | { 309 | "type": 6, 310 | "typeName": "[u8; 32]" 311 | } 312 | ] 313 | } 314 | }, 315 | "path": [ 316 | "ink_primitives", 317 | "types", 318 | "AccountId" 319 | ] 320 | } 321 | }, 322 | { 323 | "id": 6, 324 | "type": { 325 | "def": { 326 | "array": { 327 | "len": 32, 328 | "type": 7 329 | } 330 | } 331 | } 332 | }, 333 | { 334 | "id": 7, 335 | "type": { 336 | "def": { 337 | "primitive": "u8" 338 | } 339 | } 340 | }, 341 | { 342 | "id": 8, 343 | "type": { 344 | "def": { 345 | "primitive": "u128" 346 | } 347 | } 348 | }, 349 | { 350 | "id": 9, 351 | "type": { 352 | "def": { 353 | "composite": { 354 | "fields": [ 355 | { 356 | "type": 6, 357 | "typeName": "[u8; 32]" 358 | } 359 | ] 360 | } 361 | }, 362 | "path": [ 363 | "ink_primitives", 364 | "types", 365 | "Hash" 366 | ] 367 | } 368 | }, 369 | { 370 | "id": 10, 371 | "type": { 372 | "def": { 373 | "primitive": "u64" 374 | } 375 | } 376 | }, 377 | { 378 | "id": 11, 379 | "type": { 380 | "def": { 381 | "primitive": "u32" 382 | } 383 | } 384 | }, 385 | { 386 | "id": 12, 387 | "type": { 388 | "def": { 389 | "variant": {} 390 | }, 391 | "path": [ 392 | "ink_env", 393 | "types", 394 | "NoChainExtension" 395 | ] 396 | } 397 | } 398 | ], 399 | "version": "4" 400 | } 401 | -------------------------------------------------------------------------------- /examples/assets/flipper-v4.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAMdotTech/py-polkadot-sdk/a76e28b05f77dbe91032f290fee5b253197c82b1/examples/assets/flipper-v4.wasm -------------------------------------------------------------------------------- /examples/assets/flipper-v5.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0x04208d5b3de1808ed1ccf83d08bdb5e3974bba7c4af8d5fc1ed6d6c725155c2e", 4 | "language": "ink! 5.0.0", 5 | "compiler": "rustc 1.81.0-nightly", 6 | "build_info": { 7 | "build_mode": "Debug", 8 | "cargo_contract_version": "4.1.1", 9 | "rust_toolchain": "nightly-aarch64-apple-darwin", 10 | "wasm_opt_settings": { 11 | "keep_debug_symbols": false, 12 | "optimization_passes": "Z" 13 | } 14 | } 15 | }, 16 | "contract": { 17 | "name": "flipper5", 18 | "version": "0.1.0", 19 | "authors": [ 20 | "[your_name] <[your_email]>" 21 | ] 22 | }, 23 | "image": null, 24 | "spec": { 25 | "constructors": [ 26 | { 27 | "args": [ 28 | { 29 | "label": "init_value", 30 | "type": { 31 | "displayName": [ 32 | "bool" 33 | ], 34 | "type": 0 35 | } 36 | } 37 | ], 38 | "default": false, 39 | "docs": [ 40 | "Constructor that initializes the `bool` value to the given `init_value`." 41 | ], 42 | "label": "new", 43 | "payable": false, 44 | "returnType": { 45 | "displayName": [ 46 | "ink_primitives", 47 | "ConstructorResult" 48 | ], 49 | "type": 2 50 | }, 51 | "selector": "0x9bae9d5e" 52 | }, 53 | { 54 | "args": [], 55 | "default": false, 56 | "docs": [ 57 | "Constructor that initializes the `bool` value to `false`.", 58 | "", 59 | "Constructors can delegate to other constructors." 60 | ], 61 | "label": "default", 62 | "payable": false, 63 | "returnType": { 64 | "displayName": [ 65 | "ink_primitives", 66 | "ConstructorResult" 67 | ], 68 | "type": 2 69 | }, 70 | "selector": "0xed4b9d1b" 71 | } 72 | ], 73 | "docs": [], 74 | "environment": { 75 | "accountId": { 76 | "displayName": [ 77 | "AccountId" 78 | ], 79 | "type": 7 80 | }, 81 | "balance": { 82 | "displayName": [ 83 | "Balance" 84 | ], 85 | "type": 9 86 | }, 87 | "blockNumber": { 88 | "displayName": [ 89 | "BlockNumber" 90 | ], 91 | "type": 12 92 | }, 93 | "chainExtension": { 94 | "displayName": [ 95 | "ChainExtension" 96 | ], 97 | "type": 13 98 | }, 99 | "hash": { 100 | "displayName": [ 101 | "Hash" 102 | ], 103 | "type": 10 104 | }, 105 | "maxEventTopics": 4, 106 | "staticBufferSize": 16384, 107 | "timestamp": { 108 | "displayName": [ 109 | "Timestamp" 110 | ], 111 | "type": 11 112 | } 113 | }, 114 | "events": [ 115 | { 116 | "args": [ 117 | { 118 | "docs": [], 119 | "indexed": true, 120 | "label": "value", 121 | "type": { 122 | "displayName": [ 123 | "bool" 124 | ], 125 | "type": 0 126 | } 127 | } 128 | ], 129 | "docs": [], 130 | "label": "Flipped", 131 | "module_path": "flipper5::flipper5", 132 | "signature_topic": "0x529cf346ddea0543633a1d91f021fa688fb7fe023ee1fb83ad031fe005673254" 133 | }, 134 | { 135 | "args": [ 136 | { 137 | "docs": [], 138 | "indexed": true, 139 | "label": "test_value", 140 | "type": { 141 | "displayName": [ 142 | "u8" 143 | ], 144 | "type": 6 145 | } 146 | } 147 | ], 148 | "docs": [], 149 | "label": "Test", 150 | "module_path": "flipper5::flipper5", 151 | "signature_topic": "0xc04204b5a8f12647ea7e92832f7608f4b5279fbcd2181333ff6a96906e5d555f" 152 | } 153 | ], 154 | "lang_error": { 155 | "displayName": [ 156 | "ink", 157 | "LangError" 158 | ], 159 | "type": 4 160 | }, 161 | "messages": [ 162 | { 163 | "args": [], 164 | "default": false, 165 | "docs": [ 166 | " A message that can be called on instantiated contracts.", 167 | " This one flips the value of the stored `bool` from `true`", 168 | " to `false` and vice versa." 169 | ], 170 | "label": "flip", 171 | "mutates": true, 172 | "payable": false, 173 | "returnType": { 174 | "displayName": [ 175 | "ink", 176 | "MessageResult" 177 | ], 178 | "type": 2 179 | }, 180 | "selector": "0x633aa551" 181 | }, 182 | { 183 | "args": [], 184 | "default": false, 185 | "docs": [ 186 | " Simply returns the current value of our `bool`." 187 | ], 188 | "label": "get", 189 | "mutates": false, 190 | "payable": false, 191 | "returnType": { 192 | "displayName": [ 193 | "ink", 194 | "MessageResult" 195 | ], 196 | "type": 5 197 | }, 198 | "selector": "0x2f865bd9" 199 | } 200 | ] 201 | }, 202 | "storage": { 203 | "root": { 204 | "layout": { 205 | "struct": { 206 | "fields": [ 207 | { 208 | "layout": { 209 | "leaf": { 210 | "key": "0x00000000", 211 | "ty": 0 212 | } 213 | }, 214 | "name": "value" 215 | } 216 | ], 217 | "name": "Flipper5" 218 | } 219 | }, 220 | "root_key": "0x00000000", 221 | "ty": 1 222 | } 223 | }, 224 | "types": [ 225 | { 226 | "id": 0, 227 | "type": { 228 | "def": { 229 | "primitive": "bool" 230 | } 231 | } 232 | }, 233 | { 234 | "id": 1, 235 | "type": { 236 | "def": { 237 | "composite": { 238 | "fields": [ 239 | { 240 | "name": "value", 241 | "type": 0, 242 | "typeName": ",>>::Type" 243 | } 244 | ] 245 | } 246 | }, 247 | "path": [ 248 | "flipper5", 249 | "flipper5", 250 | "Flipper5" 251 | ] 252 | } 253 | }, 254 | { 255 | "id": 2, 256 | "type": { 257 | "def": { 258 | "variant": { 259 | "variants": [ 260 | { 261 | "fields": [ 262 | { 263 | "type": 3 264 | } 265 | ], 266 | "index": 0, 267 | "name": "Ok" 268 | }, 269 | { 270 | "fields": [ 271 | { 272 | "type": 4 273 | } 274 | ], 275 | "index": 1, 276 | "name": "Err" 277 | } 278 | ] 279 | } 280 | }, 281 | "params": [ 282 | { 283 | "name": "T", 284 | "type": 3 285 | }, 286 | { 287 | "name": "E", 288 | "type": 4 289 | } 290 | ], 291 | "path": [ 292 | "Result" 293 | ] 294 | } 295 | }, 296 | { 297 | "id": 3, 298 | "type": { 299 | "def": { 300 | "tuple": [] 301 | } 302 | } 303 | }, 304 | { 305 | "id": 4, 306 | "type": { 307 | "def": { 308 | "variant": { 309 | "variants": [ 310 | { 311 | "index": 1, 312 | "name": "CouldNotReadInput" 313 | } 314 | ] 315 | } 316 | }, 317 | "path": [ 318 | "ink_primitives", 319 | "LangError" 320 | ] 321 | } 322 | }, 323 | { 324 | "id": 5, 325 | "type": { 326 | "def": { 327 | "variant": { 328 | "variants": [ 329 | { 330 | "fields": [ 331 | { 332 | "type": 0 333 | } 334 | ], 335 | "index": 0, 336 | "name": "Ok" 337 | }, 338 | { 339 | "fields": [ 340 | { 341 | "type": 4 342 | } 343 | ], 344 | "index": 1, 345 | "name": "Err" 346 | } 347 | ] 348 | } 349 | }, 350 | "params": [ 351 | { 352 | "name": "T", 353 | "type": 0 354 | }, 355 | { 356 | "name": "E", 357 | "type": 4 358 | } 359 | ], 360 | "path": [ 361 | "Result" 362 | ] 363 | } 364 | }, 365 | { 366 | "id": 6, 367 | "type": { 368 | "def": { 369 | "primitive": "u8" 370 | } 371 | } 372 | }, 373 | { 374 | "id": 7, 375 | "type": { 376 | "def": { 377 | "composite": { 378 | "fields": [ 379 | { 380 | "type": 8, 381 | "typeName": "[u8; 32]" 382 | } 383 | ] 384 | } 385 | }, 386 | "path": [ 387 | "ink_primitives", 388 | "types", 389 | "AccountId" 390 | ] 391 | } 392 | }, 393 | { 394 | "id": 8, 395 | "type": { 396 | "def": { 397 | "array": { 398 | "len": 32, 399 | "type": 6 400 | } 401 | } 402 | } 403 | }, 404 | { 405 | "id": 9, 406 | "type": { 407 | "def": { 408 | "primitive": "u128" 409 | } 410 | } 411 | }, 412 | { 413 | "id": 10, 414 | "type": { 415 | "def": { 416 | "composite": { 417 | "fields": [ 418 | { 419 | "type": 8, 420 | "typeName": "[u8; 32]" 421 | } 422 | ] 423 | } 424 | }, 425 | "path": [ 426 | "ink_primitives", 427 | "types", 428 | "Hash" 429 | ] 430 | } 431 | }, 432 | { 433 | "id": 11, 434 | "type": { 435 | "def": { 436 | "primitive": "u64" 437 | } 438 | } 439 | }, 440 | { 441 | "id": 12, 442 | "type": { 443 | "def": { 444 | "primitive": "u32" 445 | } 446 | } 447 | }, 448 | { 449 | "id": 13, 450 | "type": { 451 | "def": { 452 | "variant": {} 453 | }, 454 | "path": [ 455 | "ink_env", 456 | "types", 457 | "NoChainExtension" 458 | ] 459 | } 460 | } 461 | ], 462 | "version": 5 463 | } -------------------------------------------------------------------------------- /examples/assets/flipper-v5.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAMdotTech/py-polkadot-sdk/a76e28b05f77dbe91032f290fee5b253197c82b1/examples/assets/flipper-v5.wasm -------------------------------------------------------------------------------- /examples/assets/flipper.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0x871cba462298c856adb8c2086731c7c12cddeba1f8572b90bd820bb6f8886280", 4 | "language": "ink! 3.4.0", 5 | "compiler": "rustc 1.67.0-nightly" 6 | }, 7 | "contract": { 8 | "name": "flipper", 9 | "version": "0.1.0", 10 | "authors": [ 11 | "[your_name] <[your_email]>" 12 | ] 13 | }, 14 | "V3": { 15 | "spec": { 16 | "constructors": [ 17 | { 18 | "args": [ 19 | { 20 | "label": "init_value", 21 | "type": { 22 | "displayName": [ 23 | "bool" 24 | ], 25 | "type": 0 26 | } 27 | } 28 | ], 29 | "docs": [ 30 | "Constructor that initializes the `bool` value to the given `init_value`." 31 | ], 32 | "label": "new", 33 | "payable": false, 34 | "selector": "0x9bae9d5e" 35 | }, 36 | { 37 | "args": [], 38 | "docs": [ 39 | "Constructor that initializes the `bool` value to `false`.", 40 | "", 41 | "Constructors can delegate to other constructors." 42 | ], 43 | "label": "default", 44 | "payable": false, 45 | "selector": "0xed4b9d1b" 46 | } 47 | ], 48 | "docs": [], 49 | "events": [ 50 | { 51 | "args": [ 52 | { 53 | "docs": [], 54 | "indexed": false, 55 | "label": "new_value", 56 | "type": { 57 | "displayName": [ 58 | "bool" 59 | ], 60 | "type": 0 61 | } 62 | } 63 | ], 64 | "docs": [], 65 | "label": "Flipped" 66 | } 67 | ], 68 | "messages": [ 69 | { 70 | "args": [], 71 | "docs": [ 72 | " A message that can be called on instantiated contracts.", 73 | " This one flips the value of the stored `bool` from `true`", 74 | " to `false` and vice versa." 75 | ], 76 | "label": "flip", 77 | "mutates": true, 78 | "payable": false, 79 | "returnType": null, 80 | "selector": "0x633aa551" 81 | }, 82 | { 83 | "args": [], 84 | "docs": [ 85 | " Simply returns the current value of our `bool`." 86 | ], 87 | "label": "get", 88 | "mutates": false, 89 | "payable": false, 90 | "returnType": { 91 | "displayName": [ 92 | "bool" 93 | ], 94 | "type": 0 95 | }, 96 | "selector": "0x2f865bd9" 97 | } 98 | ] 99 | }, 100 | "storage": { 101 | "struct": { 102 | "fields": [ 103 | { 104 | "layout": { 105 | "cell": { 106 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000", 107 | "ty": 0 108 | } 109 | }, 110 | "name": "value" 111 | } 112 | ] 113 | } 114 | }, 115 | "types": [ 116 | { 117 | "id": 0, 118 | "type": { 119 | "def": { 120 | "primitive": "bool" 121 | } 122 | } 123 | } 124 | ] 125 | } 126 | } -------------------------------------------------------------------------------- /examples/assets/flipper.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAMdotTech/py-polkadot-sdk/a76e28b05f77dbe91032f290fee5b253197c82b1/examples/assets/flipper.wasm -------------------------------------------------------------------------------- /examples/balance_transfer.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from substrateinterface import SubstrateInterface, Keypair 18 | from substrateinterface.exceptions import SubstrateRequestException 19 | 20 | # import logging 21 | # logging.basicConfig(level=logging.DEBUG) 22 | 23 | substrate = SubstrateInterface( 24 | url="ws://127.0.0.1:9944" 25 | ) 26 | 27 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 28 | keypair_alice = Keypair.create_from_uri('//Alice', ss58_format=substrate.ss58_format) 29 | print(keypair_alice.ss58_address) 30 | 31 | keypair = Keypair.create_from_uri('//Alice') 32 | 33 | call = substrate.compose_call( 34 | call_module='Balances', 35 | call_function='transfer_keep_alive', 36 | call_params={ 37 | 'dest': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 38 | 'value': 1 * 10**15 39 | } 40 | ) 41 | 42 | print(call.data.to_hex()) 43 | 44 | extrinsic = substrate.create_signed_extrinsic( 45 | call=call, 46 | keypair=keypair, 47 | era={'period': 64} 48 | ) 49 | 50 | try: 51 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 52 | 53 | print('Extrinsic "{}" included in block "{}"'.format( 54 | receipt.extrinsic_hash, receipt.block_hash 55 | )) 56 | 57 | if receipt.is_success: 58 | 59 | print('✅ Success, triggered events:') 60 | for event in receipt.triggered_events: 61 | print(f'* {event.value}') 62 | 63 | else: 64 | print('⚠️ Extrinsic Failed: ', receipt.error_message) 65 | 66 | 67 | except SubstrateRequestException as e: 68 | print("Failed to send: {}".format(e)) 69 | -------------------------------------------------------------------------------- /examples/batch_call.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from substrateinterface import SubstrateInterface, Keypair 18 | from substrateinterface.exceptions import SubstrateRequestException 19 | 20 | # import logging 21 | # logging.basicConfig(level=logging.DEBUG) 22 | 23 | substrate = SubstrateInterface( 24 | url="ws://127.0.0.1:9944" 25 | ) 26 | 27 | keypair = Keypair.create_from_uri('//Alice') 28 | 29 | balance_call = substrate.compose_call( 30 | call_module='Balances', 31 | call_function='transfer_keep_alive', 32 | call_params={ 33 | 'dest': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 34 | 'value': 1 * 10**15 35 | } 36 | ) 37 | 38 | call = substrate.compose_call( 39 | call_module='Utility', 40 | call_function='batch', 41 | call_params={ 42 | 'calls': [balance_call, balance_call] 43 | } 44 | ) 45 | 46 | extrinsic = substrate.create_signed_extrinsic( 47 | call=call, 48 | keypair=keypair, 49 | era={'period': 64} 50 | ) 51 | 52 | try: 53 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 54 | 55 | print('Extrinsic "{}" included in block "{}"'.format( 56 | receipt.extrinsic_hash, receipt.block_hash 57 | )) 58 | 59 | if receipt.is_success: 60 | 61 | print('✅ Success, triggered events:') 62 | for event in receipt.triggered_events: 63 | print(f'* {event.value}') 64 | 65 | else: 66 | print('⚠️ Extrinsic Failed: ', receipt.error_message) 67 | 68 | 69 | except SubstrateRequestException as e: 70 | print("Failed to send: {}".format(e)) 71 | -------------------------------------------------------------------------------- /examples/create_and_exec_contract.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | 19 | from substrateinterface.contracts import ContractCode, ContractInstance 20 | from substrateinterface import SubstrateInterface, Keypair 21 | 22 | # Enable for debugging purposes 23 | import logging 24 | logging.basicConfig(level=logging.DEBUG) 25 | 26 | substrate = SubstrateInterface(url='wss://rococo-contracts-rpc.polkadot.io') 27 | keypair = Keypair.create_from_uri('//Alice') 28 | contract_address = "5DYXHYiH5jPj8orDw5HSFJhmATe8NtmbguG3vs53v8RgSHTW" 29 | 30 | # Check if contract is on chain 31 | contract_info = substrate.query("Contracts", "ContractInfoOf", [contract_address]) 32 | 33 | if contract_info.value: 34 | 35 | print(f'Found contract on chain: {contract_info.value}') 36 | 37 | # Create contract instance from deterministic address 38 | contract = ContractInstance.create_from_address( 39 | contract_address=contract_address, 40 | metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper-v5.json'), 41 | substrate=substrate 42 | ) 43 | else: 44 | 45 | # Upload WASM code 46 | code = ContractCode.create_from_contract_files( 47 | metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper-v5.json'), 48 | wasm_file=os.path.join(os.path.dirname(__file__), 'assets', 'flipper-v5.wasm'), 49 | substrate=substrate 50 | ) 51 | 52 | # Deploy contract 53 | print('Deploy contract...') 54 | contract = code.deploy( 55 | keypair=keypair, 56 | constructor="new", 57 | args={'init_value': True}, 58 | value=0, 59 | gas_limit={'ref_time': 147523041, 'proof_size': 16689}, 60 | upload_code=True 61 | ) 62 | 63 | print(f'✅ Deployed @ {contract.contract_address}') 64 | 65 | # Read current value 66 | result = contract.read(keypair, 'get') 67 | print('Current value of "get":', result.contract_result_data) 68 | 69 | # Do a gas estimation of the message 70 | gas_predit_result = contract.read(keypair, 'flip') 71 | 72 | print('Result of dry-run: ', gas_predit_result.value) 73 | print('Gas estimate: ', gas_predit_result.gas_required) 74 | 75 | # Do the actual call 76 | print('Executing contract call...') 77 | contract_receipt = contract.exec(keypair, 'flip', args={ 78 | 79 | }, gas_limit=gas_predit_result.gas_required) 80 | 81 | if contract_receipt.is_success: 82 | print(f'Events triggered in contract: {contract_receipt.contract_events}') 83 | else: 84 | print(f'Error message: {contract_receipt.error_message}') 85 | 86 | result = contract.read(keypair, 'get') 87 | 88 | print('Current value of "get":', result.contract_result_data) 89 | -------------------------------------------------------------------------------- /examples/extensions.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from substrateinterface import SubstrateInterface 4 | from substrateinterface.extensions import SubstrateNodeExtension 5 | 6 | import logging 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | substrate = SubstrateInterface(url="wss://rpc.polkadot.io") 10 | 11 | substrate.register_extension(SubstrateNodeExtension(max_block_range=100)) 12 | 13 | # Search for block number corresponding a specific datetime 14 | block_datetime = datetime(2022, 1, 1, 0, 0, 0) 15 | block_number = substrate.extensions.search_block_number(block_datetime=block_datetime) 16 | print(f'Block number for {block_datetime}: #{block_number}') 17 | 18 | # account_info = substrate.runtime. 19 | # exit() 20 | 21 | # Returns all `Balances.Transfer` events from the last 30 blocks 22 | events = substrate.extensions.filter_events(pallet_name="Balances", event_name="Transfer", block_start=-30) 23 | print(events) 24 | 25 | # All Timestamp extrinsics in block range #3 until #6 26 | extrinsics = substrate.extensions.filter_extrinsics(pallet_name="Timestamp", block_start=3, block_end=6) 27 | print(extrinsics) 28 | -------------------------------------------------------------------------------- /examples/fee_info.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from substrateinterface import SubstrateInterface, Keypair 18 | 19 | 20 | # import logging 21 | # logging.basicConfig(level=logging.DEBUG) 22 | 23 | 24 | substrate = SubstrateInterface( 25 | url="ws://127.0.0.1:9944" 26 | ) 27 | 28 | keypair = Keypair.create_from_uri('//Alice') 29 | 30 | call = substrate.compose_call( 31 | call_module='Balances', 32 | call_function='transfer_keep_alive', 33 | call_params={ 34 | 'dest': '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 35 | 'value': 1 * 10**15 36 | } 37 | ) 38 | 39 | # Get payment info 40 | payment_info = substrate.get_payment_info(call=call, keypair=keypair) 41 | 42 | print("Payment info: ", payment_info) 43 | -------------------------------------------------------------------------------- /examples/historic_balance.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from substrateinterface import SubstrateInterface 18 | 19 | # # Enable for debugging purposes 20 | # import logging 21 | # logging.basicConfig(level=logging.DEBUG) 22 | 23 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 24 | 25 | block_number = 10 26 | block_hash = substrate.get_block_hash(block_number) 27 | 28 | result = substrate.query( 29 | "System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"], block_hash=block_hash 30 | ) 31 | 32 | 33 | def format_balance(amount: int): 34 | amount = format(amount / 10**substrate.properties.get('tokenDecimals', 0), ".15g") 35 | return f"{amount} {substrate.properties.get('tokenSymbol', 'UNIT')}" 36 | 37 | 38 | balance = (result.value["data"]["free"] + result.value["data"]["reserved"]) 39 | 40 | print(f"Balance @ {block_number}: {format_balance(balance)}") 41 | -------------------------------------------------------------------------------- /examples/multisig.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from substrateinterface import SubstrateInterface, Keypair 18 | 19 | import logging 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 23 | 24 | keypair_alice = Keypair.create_from_uri('//Alice', ss58_format=substrate.ss58_format) 25 | keypair_bob = Keypair.create_from_uri('//Bob', ss58_format=substrate.ss58_format) 26 | keypair_charlie = Keypair.create_from_uri('//Charlie', ss58_format=substrate.ss58_format) 27 | 28 | # Generate multi-sig account from signatories and threshold 29 | multisig_account = substrate.generate_multisig_account( 30 | signatories=[ 31 | keypair_alice.ss58_address, 32 | keypair_bob.ss58_address, 33 | keypair_charlie.ss58_address 34 | ], 35 | threshold=2 36 | ) 37 | 38 | call = substrate.compose_call( 39 | call_module='Balances', 40 | call_function='transfer_keep_alive', 41 | call_params={ 42 | 'dest': keypair_alice.ss58_address, 43 | 'value': 3 * 10 ** 3 44 | } 45 | ) 46 | 47 | # Initiate multisig tx 48 | extrinsic = substrate.create_multisig_extrinsic(call, keypair_alice, multisig_account, era={'period': 64}) 49 | 50 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 51 | 52 | if not receipt.is_success: 53 | print(f"⚠️ {receipt.error_message}") 54 | exit() 55 | 56 | # Finalize multisig tx with other signatory 57 | extrinsic = substrate.create_multisig_extrinsic(call, keypair_bob, multisig_account, era={'period': 64}) 58 | 59 | receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) 60 | 61 | if receipt.is_success: 62 | print(f"✅ {receipt.triggered_events}") 63 | else: 64 | print(f"⚠️ {receipt.error_message}") 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/start_local_substrate_node.sh: -------------------------------------------------------------------------------- 1 | docker run -p 9933:9933 -p 9944:9944 -p 9615:9615 -v substrate-dev:/substrate parity/substrate:2.0.0-631d4cdbca --dev --tmp --unsafe-ws-external --rpc-cors=all --unsafe-rpc-external --rpc-methods=Unsafe --prometheus-external 2 | -------------------------------------------------------------------------------- /examples/storage_subscription.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | from substrateinterface import SubstrateInterface 17 | 18 | 19 | def subscription_handler(storage_key, updated_obj, update_nr, subscription_id): 20 | print(f"Update for {storage_key}: {updated_obj.value}") 21 | 22 | 23 | substrate = SubstrateInterface(url="ws://127.0.0.1:9944") 24 | 25 | # Accounts to track 26 | storage_keys = [ 27 | substrate.create_storage_key( 28 | "System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"] 29 | ), 30 | substrate.create_storage_key( 31 | "System", "Account", ["5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"] 32 | ), 33 | substrate.create_storage_key( 34 | "System", "Events" 35 | ), 36 | ] 37 | 38 | result = substrate.subscribe_storage( 39 | storage_keys=storage_keys, subscription_handler=subscription_handler 40 | ) 41 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Python Substrate Interface Docs 2 | repo_url: https://github.com/jamdottech/py-polkadot-sdk 3 | edit_uri: edit/master/docs/ 4 | site_description: Python library to interface with the Polkadot ecosystem 5 | 6 | theme: 7 | name: "material" 8 | logo: https://avatars.githubusercontent.com/u/43450475 9 | features: 10 | # - announce.dismiss 11 | - content.action.edit 12 | - content.action.view 13 | - content.code.annotate 14 | - content.code.copy 15 | # - content.tabs.link 16 | - content.tooltips 17 | # - header.autohide 18 | - navigation.expand 19 | - navigation.footer 20 | - navigation.indexes 21 | - navigation.instant 22 | - navigation.prune 23 | - navigation.sections 24 | - navigation.tabs 25 | - navigation.tabs.sticky 26 | - navigation.top 27 | - navigation.tracking 28 | - search.highlight 29 | - search.share 30 | - search.suggest 31 | - toc.follow 32 | # - toc.integrate 33 | 34 | plugins: 35 | - mkdocstrings: 36 | handlers: 37 | python: 38 | options: 39 | # docstring_section_style: list 40 | members_order: source 41 | show_root_heading: false 42 | show_source: false 43 | show_signature_annotations: true 44 | docstring_style: numpy 45 | heading_level: 2 46 | 47 | - autorefs 48 | - search 49 | 50 | extra: 51 | social: 52 | - icon: fontawesome/brands/github 53 | link: https://github.com/JAMdotTech 54 | - icon: fontawesome/brands/twitter 55 | link: https://twitter.com/JAMdotTech 56 | 57 | nav: 58 | - Overview: index.md 59 | - Getting started: 60 | - getting-started/installation.md 61 | - getting-started/common-concepts.md 62 | - Usage: 63 | - usage/query-storage.md 64 | - usage/using-scaletype-objects.md 65 | - usage/subscriptions.md 66 | - usage/keypair-creation-and-signing.md 67 | - usage/extrinsics.md 68 | - usage/call-runtime-apis.md 69 | - usage/ink-contract-interfacing.md 70 | - usage/extensions.md 71 | - usage/cleanup-and-context-manager.md 72 | - Function Reference: 73 | - reference/base.md 74 | - reference/keypair.md 75 | - reference/contracts.md 76 | - reference/interfaces.md 77 | - reference/extensions.md 78 | - reference/storage.md 79 | - Examples: examples.md 80 | - Extensions: 81 | - extensions/index.md 82 | - extensions/substrate-node-extension.md 83 | - extensions/polkascan-extension.md 84 | - extensions/subsquid-extension.md 85 | - Metadata docs: https://jamdottech.github.io/py-polkadot-metadata-docs/ 86 | 87 | 88 | markdown_extensions: 89 | - toc: 90 | permalink: true 91 | toc_depth: 4 92 | - pymdownx.highlight: 93 | linenums: true 94 | - pymdownx.inlinehilite 95 | - pymdownx.snippets 96 | - pymdownx.superfences 97 | 98 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client>=0.57.0,<2 2 | base58>=1.0.3,<3 3 | certifi>=2019.3.9 4 | idna>=2.1.0,<4 5 | requests>=2.21.0,<3 6 | xxhash>=1.3.0,<4 7 | pytest>=4.4.0 8 | ecdsa>=0.17.0,<1 9 | eth-keys>=0.2.1,<1 10 | eth_utils>=1.3.0,<6 11 | pycryptodome>=3.11.0,<4 12 | PyNaCl>=1.0.1,<2 13 | 14 | scalecodec>=1.2.10,<1.3 15 | py-sr25519-bindings>=0.2.0,<1 16 | py-ed25519-zebra-bindings>=1.0,<2 17 | py-bip39-bindings>=0.1.9,<1 18 | 19 | mkdocs 20 | mkdocs-material 21 | mkdocs-autorefs 22 | mkdocstrings 23 | mkdocstrings[python] 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """A setuptools based setup module. 18 | 19 | See: 20 | https://packaging.python.org/guides/distributing-packages-using-setuptools/ 21 | https://github.com/pypa/sampleproject 22 | """ 23 | 24 | # Always prefer setuptools over distutils 25 | from setuptools import setup, find_packages 26 | from os import path, environ 27 | # io.open is needed for projects that support Python 2.7 28 | # It ensures open() defaults to text mode with universal newlines, 29 | # and accepts an argument to specify the text encoding 30 | # Python 3 only projects can skip this import 31 | from io import open 32 | 33 | if environ.get('TRAVIS_TAG'): 34 | version = environ['TRAVIS_TAG'].replace('v', '') 35 | elif environ.get('CI_COMMIT_TAG'): 36 | version = environ['CI_COMMIT_TAG'].replace('v', '') 37 | elif environ.get('GITHUB_REF'): 38 | 39 | if not environ['GITHUB_REF'].startswith('refs/tags/v'): 40 | raise ValueError('Incorrect tag format {}'.format(environ['GITHUB_REF'])) 41 | 42 | version = environ['GITHUB_REF'].replace('refs/tags/v', '') 43 | else: 44 | raise ValueError('Missing commit tag, can\'t set version') 45 | 46 | here = path.abspath(path.dirname(__file__)) 47 | 48 | # Get the long description from the README file 49 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 50 | long_description = f.read() 51 | 52 | # Arguments marked as "Required" below must be included for upload to PyPI. 53 | # Fields marked as "Optional" may be commented out. 54 | 55 | setup( 56 | # This is the name of your project. The first time you publish this 57 | # package, this name will be registered for you. It will determine how 58 | # users can install this project, e.g.: 59 | # 60 | # $ pip install sampleproject 61 | # 62 | # And where it will live on PyPI: https://pypi.org/project/sampleproject/ 63 | # 64 | # There are some restrictions on what makes a valid project name 65 | # specification here: 66 | # https://packaging.python.org/specifications/core-metadata/#name 67 | name='substrate-interface', # Required 68 | 69 | # Versions should comply with PEP 440: 70 | # https://www.python.org/dev/peps/pep-0440/ 71 | # 72 | # For a discussion on single-sourcing the version across setup.py and the 73 | # project code, see 74 | # https://packaging.python.org/en/latest/single_source_version.html 75 | version=version, # Required 76 | 77 | # This is a one-line description or tagline of what your project does. This 78 | # corresponds to the "Summary" metadata field: 79 | # https://packaging.python.org/specifications/core-metadata/#summary 80 | description='Library for interfacing with a Substrate node', # Optional 81 | 82 | # This is an optional longer description of your project that represents 83 | # the body of text which users will see when they visit PyPI. 84 | # 85 | # Often, this is the same as your README, so you can just read it in from 86 | # that file directly (as we have already done above) 87 | # 88 | # This field corresponds to the "Description" metadata field: 89 | # https://packaging.python.org/specifications/core-metadata/#description-optional 90 | long_description=long_description, # Optional 91 | 92 | # Denotes that our long_description is in Markdown; valid values are 93 | # text/plain, text/x-rst, and text/markdown 94 | # 95 | # Optional if long_description is written in reStructuredText (rst) but 96 | # required for plain-text or Markdown; if unspecified, "applications should 97 | # attempt to render [the long_description] as text/x-rst; charset=UTF-8 and 98 | # fall back to text/plain if it is not valid rst" (see link below) 99 | # 100 | # This field corresponds to the "Description-Content-Type" metadata field: 101 | # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional 102 | long_description_content_type='text/markdown', # Optional (see note above) 103 | 104 | # This should be a valid link to your project's main homepage. 105 | # 106 | # This field corresponds to the "Home-Page" metadata field: 107 | # https://packaging.python.org/specifications/core-metadata/#home-page-optional 108 | url='https://github.com/polkascan/py-substrate-interface', # Optional 109 | 110 | # This should be your name or the name of the organization which owns the 111 | # project. 112 | author='Stichting Polkascan (Polkascan Foundation)', # Optional 113 | 114 | # This should be a valid email address corresponding to the author listed 115 | # above. 116 | author_email='info@polkascan.org', # Optional 117 | 118 | # Classifiers help users find your project by categorizing it. 119 | # 120 | # For a list of valid classifiers, see https://pypi.org/classifiers/ 121 | classifiers=[ # Optional 122 | # How mature is this project? Common values are 123 | # 3 - Alpha 124 | # 4 - Beta 125 | # 5 - Production/Stable 126 | 'Development Status :: 5 - Production/Stable', 127 | 128 | # Indicate who your project is intended for 129 | 'Intended Audience :: Developers', 130 | 131 | # Pick your license as you wish 132 | 'License :: OSI Approved :: Apache Software License', 133 | 134 | # Specify the Python versions you support here. In particular, ensure 135 | # that you indicate whether you support Python 2, Python 3 or both. 136 | # These classifiers are *not* checked by 'pip install'. See instead 137 | # 'python_requires' below. 138 | 'Programming Language :: Python :: 3', 139 | 'Programming Language :: Python :: 3.7', 140 | 'Programming Language :: Python :: 3.8', 141 | 'Programming Language :: Python :: 3.9', 142 | 'Programming Language :: Python :: 3.10', 143 | 'Programming Language :: Python :: 3.11', 144 | ], 145 | 146 | # This field adds keywords for your project which will appear on the 147 | # project page. What does your project relate to? 148 | # 149 | # Note that this is a string of words separated by whitespace, not a list. 150 | keywords='interface polkascan polkadot substrate blockchain rpc kusama', # Optional 151 | 152 | # You can just specify package directories manually here if your project is 153 | # simple. Or you can use find_packages(). 154 | # 155 | # Alternatively, if you just want to distribute a single Python file, use 156 | # the `py_modules` argument instead as follows, which will expect a file 157 | # called `my_module.py` to exist: 158 | # 159 | # py_modules=["my_module"], 160 | # 161 | #packages=find_packages(exclude=['contrib', 'docs', 'tests', 'test']), # Required 162 | packages=find_packages(exclude=['contrib', 'docs', 'tests', 'test']), # Required 163 | 164 | # Specify which Python versions you support. In contrast to the 165 | # 'Programming Language' classifiers above, 'pip install' will check this 166 | # and refuse to install the project if the version does not match. If you 167 | # do not support Python 2, you can simplify this to '>=3.5' or similar, see 168 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires 169 | 170 | #python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', 171 | python_requires='>=3.7, <4', 172 | 173 | # This field lists other packages that your project depends on to run. 174 | # Any package you put here will be installed by pip when your project is 175 | # installed, so they must be valid existing projects. 176 | # 177 | # For an analysis of "install_requires" vs pip's requirements files see: 178 | # https://packaging.python.org/en/latest/requirements.html 179 | install_requires=[ 180 | 'websocket-client>=0.57.0,<2', 181 | 'base58>=1.0.3,<3', 182 | 'certifi>=2019.3.9', 183 | 'idna>=2.1.0,<4', 184 | 'requests>=2.21.0,<3', 185 | 'xxhash>=1.3.0,<4', 186 | 'ecdsa>=0.17.0,<1', 187 | 'eth-keys>=0.2.1,<1', 188 | 'eth_utils>=1.3.0,<6', 189 | 'pycryptodome>=3.11.0,<4', 190 | 'PyNaCl>=1.0.1,<2', 191 | 'scalecodec>=1.2.10,<1.3', 192 | 'py-sr25519-bindings>=0.2.0,<1', 193 | 'py-ed25519-zebra-bindings>=1.0,<2', 194 | 'py-bip39-bindings>=0.1.9,<1' 195 | ], 196 | 197 | # List additional groups of dependencies here (e.g. development 198 | # dependencies). Users will be able to install these using the "extras" 199 | # syntax, for example: 200 | # 201 | # $ pip install sampleproject[dev] 202 | # 203 | # Similar to `install_requires` above, these must be valid existing 204 | # projects. 205 | extras_require={ # Optional 206 | #'dev': ['check-manifest'], 207 | 'test': ['coverage', 'pytest'], 208 | }, 209 | 210 | # If there are data files included in your packages that need to be 211 | # installed, specify them here. 212 | # 213 | # If using Python 2.6 or earlier, then these have to be included in 214 | # MANIFEST.in as well. 215 | 216 | # package_data={ # Optional 217 | # 'sample': ['package_data.dat'], 218 | # }, 219 | 220 | # Although 'package_data' is the preferred approach, in some case you may 221 | # need to place data files outside of your packages. See: 222 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files 223 | # 224 | # In this case, 'data_file' will be installed into '/my_data' 225 | # data_files=[('my_data', ['data/data_file'])], # Optional 226 | 227 | # To provide executable scripts, use entry points in preference to the 228 | # "scripts" keyword. Entry points provide cross-platform support and allow 229 | # `pip` to create the appropriate form of executable for the target 230 | # platform. 231 | # 232 | # For example, the following would provide a command called `sample` which 233 | # executes the function `main` from this package when invoked: 234 | 235 | # entry_points={ # Optional 236 | # 'console_scripts': [ 237 | # 'sample=sample:main', 238 | # ], 239 | # }, 240 | 241 | # List additional URLs that are relevant to your project as a dict. 242 | # 243 | # This field corresponds to the "Project-URL" metadata fields: 244 | # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use 245 | # 246 | # Examples listed include a pattern for specifying where the package tracks 247 | # issues, where the source is hosted, where to say thanks to the package 248 | # maintainers, and where to support the project financially. The key is 249 | # what's used to render the link text on PyPI. 250 | # project_urls={ # Optional 251 | # 'Bug Reports': 'https://github.com/pypa/sampleproject/issues', 252 | # 'Funding': 'https://donate.pypi.org', 253 | # 'Say Thanks!': 'http://saythanks.io/to/example', 254 | # 'Source': 'https://github.com/pypa/sampleproject/', 255 | # }, 256 | ) 257 | -------------------------------------------------------------------------------- /substrateinterface/__init__.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from .base import * 18 | from .contracts import * 19 | from .keypair import * 20 | from .interfaces import * 21 | from .extensions import * 22 | 23 | __all__ = (base.__all__ + contracts.__all__ + keypair.__all__ + interfaces.__all__ + extensions.__all__) 24 | -------------------------------------------------------------------------------- /substrateinterface/constants.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | STORAGE_HASH_SYSTEM_EVENTS = "0xcc956bdb7605e3547539f321ac2bc95c" 18 | STORAGE_HASH_SYSTEM_EVENTS_V9 = "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7" 19 | 20 | DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk' 21 | 22 | WELL_KNOWN_STORAGE_KEYS = { 23 | "Code": { 24 | "storage_key": "0x3a636f6465", 25 | "value_type_string": "RawBytes", 26 | "docs": "Wasm code of the runtime", 27 | "default": '0x' 28 | }, 29 | "HeapPages": { 30 | "storage_key": "0x3a686561707061676573", 31 | "value_type_string": "u64", 32 | "docs": "Number of wasm linear memory pages required for execution of the runtime.", 33 | "default": "0x0000000000000000" 34 | }, 35 | "ExtrinsicIndex": { 36 | "storage_key": "0x3a65787472696e7369635f696e646578", 37 | "value_type_string": "u32", 38 | "docs": "Number of wasm linear memory pages required for execution of the runtime.", 39 | "default": "0x00000000" 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /substrateinterface/exceptions.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | class SubstrateRequestException(Exception): 19 | pass 20 | 21 | 22 | class StorageFunctionNotFound(ValueError): 23 | pass 24 | 25 | 26 | class ConfigurationError(Exception): 27 | pass 28 | 29 | 30 | class ExtrinsicFailedException(Exception): 31 | pass 32 | 33 | 34 | class DeployContractFailedException(Exception): 35 | pass 36 | 37 | 38 | class ContractMetadataParseException(ValueError): 39 | pass 40 | 41 | 42 | class ContractReadFailedException(Exception): 43 | pass 44 | 45 | 46 | class ContractExecFailedException(Exception): 47 | pass 48 | 49 | 50 | class BlockNotFound(Exception): 51 | pass 52 | 53 | 54 | class ExtrinsicNotFound(Exception): 55 | pass 56 | 57 | 58 | class ExtensionCallNotFound(AttributeError): 59 | pass 60 | -------------------------------------------------------------------------------- /substrateinterface/extensions.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | from typing import TYPE_CHECKING 17 | 18 | import math 19 | from datetime import datetime, timedelta 20 | 21 | __all__ = ['Extension', 'SearchExtension', 'SubstrateNodeExtension'] 22 | 23 | if TYPE_CHECKING: 24 | from .base import SubstrateInterface 25 | 26 | 27 | class Extension: 28 | """ 29 | Base class of all extensions 30 | """ 31 | def __init__(self): 32 | self.substrate = None 33 | 34 | def init(self, substrate: 'SubstrateInterface'): 35 | """ 36 | Initialization process of the extension. This function is being called by the ExtensionRegistry. 37 | 38 | Parameters 39 | ---------- 40 | substrate: SubstrateInterface 41 | 42 | Returns 43 | ------- 44 | 45 | """ 46 | self.substrate: 'SubstrateInterface' = substrate 47 | 48 | def close(self): 49 | """ 50 | Cleanup process of the extension. This function is being called by the ExtensionRegistry. 51 | 52 | Returns 53 | ------- 54 | 55 | """ 56 | pass 57 | 58 | def debug_message(self, message: str): 59 | """ 60 | Submits a debug message in the logger 61 | 62 | Parameters 63 | ---------- 64 | message: str 65 | 66 | Returns 67 | ------- 68 | 69 | """ 70 | self.substrate.debug_message(f'Extension {self.__class__.__name__}: {message}') 71 | 72 | 73 | class SearchExtension(Extension): 74 | """ 75 | Type of `Extension` that implements functionality to improve and enhance search capability 76 | """ 77 | 78 | def filter_events(self, **kwargs) -> list: 79 | """ 80 | Filters events to match provided search criteria e.g. block range, pallet name, accountID in attributes 81 | 82 | Parameters 83 | ---------- 84 | kwargs 85 | 86 | Returns 87 | ------- 88 | list 89 | """ 90 | raise NotImplementedError() 91 | 92 | def filter_extrinsics(self, **kwargs) -> list: 93 | """ 94 | Filters extrinsics to match provided search criteria e.g. block range, pallet name, signed by accountID 95 | 96 | Parameters 97 | ---------- 98 | kwargs 99 | 100 | Returns 101 | ------- 102 | 103 | """ 104 | raise NotImplementedError() 105 | 106 | def search_block_number(self, block_datetime: datetime, block_time: int = 6, **kwargs) -> int: 107 | """ 108 | Search corresponding block number for provided `block_datetime`. the prediction tolerance is provided with 109 | `block_time` 110 | 111 | Parameters 112 | ---------- 113 | block_datetime: datetime 114 | block_time: int 115 | kwargs 116 | 117 | Returns 118 | ------- 119 | int 120 | """ 121 | raise NotImplementedError() 122 | 123 | def get_block_timestamp(self, block_number: int) -> int: 124 | """ 125 | Return a UNIX timestamp for given `block_number`. 126 | 127 | Parameters 128 | ---------- 129 | block_number: int The block_number to retrieve the timestamp for 130 | 131 | Returns 132 | ------- 133 | int 134 | """ 135 | raise NotImplementedError() 136 | 137 | 138 | class SubstrateNodeExtension(SearchExtension): 139 | """ 140 | Implementation of `SearchExtension` using only Substrate RPC methods. Could be significant inefficient. 141 | """ 142 | 143 | def filter_extrinsics(self, block_start: int = None, block_end: int = None, ss58_address: str = None, 144 | pallet_name: str = None, call_name: str = None) -> list: 145 | 146 | if block_end is None: 147 | block_end = self.substrate.get_block_number(None) 148 | 149 | if block_start is None: 150 | block_start = block_end 151 | 152 | if block_start < 0: 153 | block_start += block_end 154 | 155 | result = [] 156 | 157 | for block_number in range(block_start, block_end + 1): 158 | block_hash = self.substrate.get_block_hash(block_number) 159 | 160 | for extrinsic in self.substrate.get_extrinsics(block_hash=block_hash): 161 | if pallet_name is not None and pallet_name != extrinsic.value['call']['call_module']: 162 | continue 163 | 164 | if call_name is not None and call_name != extrinsic.value['call']['call_function']: 165 | continue 166 | 167 | result.append(extrinsic) 168 | 169 | return result 170 | 171 | def __init__(self, max_block_range: int = 100): 172 | super().__init__() 173 | 174 | self.max_block_range: int = max_block_range 175 | 176 | def filter_events(self, block_start: int = None, block_end: int = None, pallet_name: str = None, 177 | event_name: str = None, account_id: str = None) -> list: 178 | 179 | if block_end is None: 180 | block_end = self.substrate.get_block_number(None) 181 | 182 | if block_start is None: 183 | block_start = block_end 184 | 185 | if block_start < 0: 186 | block_start += block_end 187 | 188 | # Requirements check 189 | if block_end - block_start > self.max_block_range: 190 | raise ValueError(f"max_block_range ({self.max_block_range}) exceeded") 191 | 192 | result = [] 193 | 194 | self.debug_message(f"Retrieving events from #{block_start} to #{block_end}") 195 | 196 | for block_number in range(block_start, block_end + 1): 197 | block_hash = self.substrate.get_block_hash(block_number) 198 | for event in self.substrate.get_events(block_hash=block_hash): 199 | if pallet_name is not None and pallet_name != event.value['event']['module_id']: 200 | continue 201 | 202 | if event_name is not None and event_name != event.value['event']['event_id']: 203 | continue 204 | 205 | # if account_id is not None: 206 | # found = False 207 | # for param in event.params: 208 | # if param['type'] == 'AccountId' and param['value'] == account_id: 209 | # found = True 210 | # break 211 | # if not found: 212 | # continue 213 | 214 | result.append(event) 215 | 216 | return result 217 | 218 | def get_block_timestamp(self, block_number: int) -> int: 219 | extrinsics = self.filter_extrinsics( 220 | block_start=block_number, block_end=block_number, pallet_name="Timestamp", 221 | call_name="set" 222 | ) 223 | return extrinsics[0].value['call']['call_args'][0]['value'] / 1000 224 | 225 | def search_block_number(self, block_datetime: datetime, block_time: int = 6, **kwargs) -> int: 226 | """ 227 | Search corresponding block number for provided `block_datetime`. the prediction tolerance is provided with 228 | `block_time` 229 | 230 | Parameters 231 | ---------- 232 | block_datetime: datetime 233 | block_time: int 234 | kwargs 235 | 236 | Returns 237 | ------- 238 | int 239 | """ 240 | accuracy = timedelta(seconds=block_time) 241 | 242 | target_block_timestamp = block_datetime.timestamp() 243 | 244 | # Retrieve Timestamp extrinsic for chain tip 245 | predicted_block_number = self.substrate.get_block_number(None) 246 | current_timestamp = self.get_block_timestamp(predicted_block_number) 247 | current_delta = current_timestamp - target_block_timestamp 248 | 249 | self.debug_message(f"Delta {current_delta} sec with chain tip #{predicted_block_number}") 250 | 251 | if current_delta < 0: 252 | raise ValueError("Requested block_datetime is higher than current chain tip") 253 | 254 | while accuracy < timedelta(seconds=math.fabs(current_delta)): 255 | 256 | predicted_block_number = math.ceil(predicted_block_number - current_delta / block_time) 257 | 258 | if predicted_block_number < 0: 259 | raise ValueError(f"Requested datetime points before genesis of chain (#{predicted_block_number})") 260 | 261 | current_timestamp = self.get_block_timestamp(predicted_block_number) 262 | 263 | # Predict target block number 264 | current_delta = current_timestamp - target_block_timestamp 265 | 266 | self.debug_message(f"Current delta {current_delta} sec; predicted #{predicted_block_number}") 267 | 268 | self.debug_message(f"Accepted prediction #{predicted_block_number}") 269 | 270 | return predicted_block_number 271 | 272 | 273 | # Backwards compatibility 274 | class SubstrateNodeSearchExtension(SubstrateNodeExtension): 275 | pass 276 | -------------------------------------------------------------------------------- /substrateinterface/interfaces.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from typing import Callable 18 | 19 | from .extensions import Extension 20 | from .exceptions import ExtensionCallNotFound 21 | 22 | __all__ = ['ExtensionInterface'] 23 | 24 | 25 | class ExtensionInterface: 26 | """ 27 | Keeps tracks of active extensions and which calls can be made 28 | """ 29 | 30 | def __init__(self, substrate): 31 | self.substrate = substrate 32 | self.extensions = [] 33 | 34 | def __len__(self): 35 | return len(self.extensions) 36 | 37 | def __iter__(self): 38 | for item in self.extensions: 39 | yield item 40 | 41 | def __add__(self, other): 42 | self.register(other) 43 | return self 44 | 45 | def register(self, extension: Extension): 46 | """ 47 | Register an extension instance to the registry and calls initialization 48 | 49 | Parameters 50 | ---------- 51 | extension: Extension 52 | 53 | Returns 54 | ------- 55 | 56 | """ 57 | if not isinstance(extension, Extension): 58 | raise ValueError("Provided extension is not a subclass of Extension") 59 | 60 | extension.init(self.substrate) 61 | 62 | self.extensions.append(extension) 63 | 64 | def unregister_all(self): 65 | """ 66 | Unregister all extensions and free used resources and connections 67 | 68 | Returns 69 | ------- 70 | 71 | """ 72 | for extension in self.extensions: 73 | extension.close() 74 | 75 | def call(self, name: str, *args, **kwargs): 76 | """ 77 | Tries to call extension function with `name` and provided args and kwargs 78 | 79 | Will raise a `ExtensionCallNotFound` when no method is found in current extensions 80 | 81 | Parameters 82 | ---------- 83 | name 84 | args 85 | kwargs 86 | 87 | Returns 88 | ------- 89 | 90 | """ 91 | return self.get_extension_callable(name)(*args, **kwargs) 92 | 93 | def get_extension_callable(self, name: str) -> Callable: 94 | 95 | for extension in self.extensions: 96 | if isinstance(extension, Extension): 97 | if hasattr(extension, name): 98 | try: 99 | # Call extension that implements functionality 100 | self.substrate.debug_message(f"Call '{name}' using extension {extension.__class__.__name__} ...") 101 | return getattr(extension, name) 102 | except NotImplementedError: 103 | pass 104 | 105 | raise ExtensionCallNotFound(f"No extension registered that implements call '{name}'") 106 | 107 | def __getattr__(self, name): 108 | return self.get_extension_callable(name) 109 | -------------------------------------------------------------------------------- /substrateinterface/key.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | import re 16 | from hashlib import blake2b 17 | from math import ceil 18 | 19 | from scalecodec.types import Bytes 20 | 21 | RE_JUNCTION = r'(\/\/?)([^/]+)' 22 | JUNCTION_ID_LEN = 32 23 | 24 | 25 | class DeriveJunction: 26 | def __init__(self, chain_code, is_hard=False): 27 | self.chain_code = chain_code 28 | self.is_hard = is_hard 29 | 30 | @classmethod 31 | def from_derive_path(cls, path: str, is_hard=False): 32 | 33 | if path.isnumeric(): 34 | byte_length = ceil(int(path).bit_length() / 8) 35 | chain_code = int(path).to_bytes(byte_length, 'little').ljust(32, b'\x00') 36 | 37 | else: 38 | path_scale = Bytes() 39 | path_scale.encode(path) 40 | 41 | if len(path_scale.data) > JUNCTION_ID_LEN: 42 | chain_code = blake2b(path_scale.data.data, digest_size=32).digest() 43 | else: 44 | chain_code = bytes(path_scale.data.data.ljust(32, b'\x00')) 45 | 46 | return cls(chain_code=chain_code, is_hard=is_hard) 47 | 48 | 49 | def extract_derive_path(derive_path: str): 50 | 51 | path_check = '' 52 | junctions = [] 53 | paths = re.findall(RE_JUNCTION, derive_path) 54 | 55 | if paths: 56 | path_check = ''.join(''.join(path) for path in paths) 57 | 58 | for path_separator, path_value in paths: 59 | junctions.append(DeriveJunction.from_derive_path( 60 | path=path_value, is_hard=path_separator == '//') 61 | ) 62 | 63 | if path_check != derive_path: 64 | raise ValueError('Reconstructed path "{}" does not match input'.format(path_check)) 65 | 66 | return junctions 67 | 68 | -------------------------------------------------------------------------------- /substrateinterface/storage.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import binascii 17 | from typing import Any, Optional 18 | 19 | from substrateinterface.exceptions import StorageFunctionNotFound 20 | 21 | from scalecodec import ScaleBytes, GenericMetadataVersioned, ss58_decode 22 | from scalecodec.base import ScaleDecoder, RuntimeConfigurationObject, ScaleType 23 | from .utils.hasher import blake2_256, two_x64_concat, xxh128, blake2_128, blake2_128_concat, identity 24 | 25 | 26 | class StorageKey: 27 | """ 28 | A StorageKey instance is a representation of a single state entry. 29 | 30 | Substrate uses a simple key-value data store implemented as a database-backed, modified Merkle tree. 31 | All of Substrate's higher-level storage abstractions are built on top of this simple key-value store. 32 | """ 33 | 34 | def __init__( 35 | self, pallet: str, storage_function: str, params: list, 36 | data: bytes, value_scale_type: str, metadata: GenericMetadataVersioned, 37 | runtime_config: RuntimeConfigurationObject 38 | ): 39 | self.pallet = pallet 40 | self.storage_function = storage_function 41 | self.params = params 42 | self.params_encoded = [] 43 | self.data = data 44 | self.metadata = metadata 45 | self.runtime_config = runtime_config 46 | self.value_scale_type = value_scale_type 47 | self.metadata_storage_function = None 48 | 49 | @classmethod 50 | def create_from_data(cls, data: bytes, runtime_config: RuntimeConfigurationObject, 51 | metadata: GenericMetadataVersioned, value_scale_type: str = None, pallet: str = None, 52 | storage_function: str = None) -> 'StorageKey': 53 | """ 54 | Create a StorageKey instance providing raw storage key bytes 55 | 56 | Parameters 57 | ---------- 58 | data: bytes representation of the storage key 59 | runtime_config: RuntimeConfigurationObject 60 | metadata: GenericMetadataVersioned 61 | value_scale_type: type string of to decode result data 62 | pallet: name of pallet 63 | storage_function: name of storage function 64 | 65 | Returns 66 | ------- 67 | StorageKey 68 | """ 69 | if not value_scale_type and pallet and storage_function: 70 | metadata_pallet = metadata.get_metadata_pallet(pallet) 71 | 72 | if not metadata_pallet: 73 | raise StorageFunctionNotFound(f'Pallet "{pallet}" not found') 74 | 75 | storage_item = metadata_pallet.get_storage_function(storage_function) 76 | 77 | if not storage_item: 78 | raise StorageFunctionNotFound(f'Storage function "{pallet}.{storage_function}" not found') 79 | 80 | # Process specific type of storage function 81 | value_scale_type = storage_item.get_value_type_string() 82 | 83 | return cls( 84 | pallet=None, storage_function=None, params=None, 85 | data=data, metadata=metadata, 86 | value_scale_type=value_scale_type, runtime_config=runtime_config 87 | ) 88 | 89 | @classmethod 90 | def create_from_storage_function(cls, pallet: str, storage_function: str, params: list, 91 | runtime_config: RuntimeConfigurationObject, 92 | metadata: GenericMetadataVersioned) -> 'StorageKey': 93 | """ 94 | Create a StorageKey instance providing storage function details 95 | 96 | Parameters 97 | ---------- 98 | pallet: name of pallet 99 | storage_function: name of storage function 100 | params: Optional list of parameters in case of a Mapped storage function 101 | runtime_config: RuntimeConfigurationObject 102 | metadata: GenericMetadataVersioned 103 | 104 | Returns 105 | ------- 106 | StorageKey 107 | """ 108 | storage_key_obj = cls( 109 | pallet=pallet, storage_function=storage_function, params=params, 110 | data=None, runtime_config=runtime_config, metadata=metadata, value_scale_type=None 111 | ) 112 | 113 | storage_key_obj.generate() 114 | 115 | return storage_key_obj 116 | 117 | def convert_storage_parameter(self, scale_type: str, value: Any): 118 | 119 | if type(value) is bytes: 120 | value = f'0x{value.hex()}' 121 | 122 | if scale_type == 'AccountId': 123 | if value[0:2] != '0x': 124 | return '0x{}'.format(ss58_decode(value, self.runtime_config.ss58_format)) 125 | 126 | return value 127 | 128 | def to_hex(self) -> str: 129 | """ 130 | Returns a Hex-string representation of current StorageKey data 131 | 132 | Returns 133 | ------- 134 | str 135 | Hex string 136 | """ 137 | if self.data: 138 | return f'0x{self.data.hex()}' 139 | 140 | def generate(self) -> bytes: 141 | """ 142 | Generate a storage key for current specified pallet/function/params 143 | 144 | Returns 145 | ------- 146 | bytes 147 | """ 148 | 149 | # Search storage call in metadata 150 | metadata_pallet = self.metadata.get_metadata_pallet(self.pallet) 151 | 152 | if not metadata_pallet: 153 | raise StorageFunctionNotFound(f'Pallet "{self.pallet}" not found') 154 | 155 | self.metadata_storage_function = metadata_pallet.get_storage_function(self.storage_function) 156 | 157 | if not self.metadata_storage_function: 158 | raise StorageFunctionNotFound(f'Storage function "{self.pallet}.{self.storage_function}" not found') 159 | 160 | # Process specific type of storage function 161 | self.value_scale_type = self.metadata_storage_function.get_value_type_string() 162 | param_types = self.metadata_storage_function.get_params_type_string() 163 | 164 | hashers = self.metadata_storage_function.get_param_hashers() 165 | 166 | storage_hash = xxh128(metadata_pallet.value['storage']['prefix'].encode()) + xxh128(self.storage_function.encode()) 167 | 168 | # Encode parameters 169 | self.params_encoded = [] 170 | if self.params: 171 | for idx, param in enumerate(self.params): 172 | if type(param) is ScaleBytes: 173 | # Already encoded 174 | self.params_encoded.append(param) 175 | else: 176 | param = self.convert_storage_parameter(param_types[idx], param) 177 | param_obj = self.runtime_config.create_scale_object(type_string=param_types[idx]) 178 | self.params_encoded.append(param_obj.encode(param)) 179 | 180 | for idx, param in enumerate(self.params_encoded): 181 | # Get hasher assiociated with param 182 | try: 183 | param_hasher = hashers[idx] 184 | except IndexError: 185 | raise ValueError(f'No hasher found for param #{idx + 1}') 186 | 187 | params_key = bytes() 188 | 189 | # Convert param to bytes 190 | if type(param) is str: 191 | params_key += binascii.unhexlify(param) 192 | elif type(param) is ScaleBytes: 193 | params_key += param.data 194 | elif isinstance(param, ScaleDecoder): 195 | params_key += param.data.data 196 | 197 | if not param_hasher: 198 | param_hasher = 'Twox128' 199 | 200 | if param_hasher == 'Blake2_256': 201 | storage_hash += blake2_256(params_key) 202 | 203 | elif param_hasher == 'Blake2_128': 204 | storage_hash += blake2_128(params_key) 205 | 206 | elif param_hasher == 'Blake2_128Concat': 207 | storage_hash += blake2_128_concat(params_key) 208 | 209 | elif param_hasher == 'Twox128': 210 | storage_hash += xxh128(params_key) 211 | 212 | elif param_hasher == 'Twox64Concat': 213 | storage_hash += two_x64_concat(params_key) 214 | 215 | elif param_hasher == 'Identity': 216 | storage_hash += identity(params_key) 217 | 218 | else: 219 | raise ValueError('Unknown storage hasher "{}"'.format(param_hasher)) 220 | 221 | self.data = storage_hash 222 | 223 | return self.data 224 | 225 | def decode_scale_value(self, data: Optional[ScaleBytes] = None) -> ScaleType: 226 | """ 227 | 228 | Parameters 229 | ---------- 230 | data 231 | 232 | Returns 233 | ------- 234 | 235 | """ 236 | 237 | result_found = False 238 | 239 | if data is not None: 240 | change_scale_type = self.value_scale_type 241 | result_found = True 242 | elif self.metadata_storage_function.value['modifier'] == 'Default': 243 | # Fallback to default value of storage function if no result 244 | change_scale_type = self.value_scale_type 245 | data = ScaleBytes(self.metadata_storage_function.value_object['default'].value_object) 246 | else: 247 | # No result is interpreted as an Option<...> result 248 | change_scale_type = f'Option<{self.value_scale_type}>' 249 | data = ScaleBytes(self.metadata_storage_function.value_object['default'].value_object) 250 | 251 | # Decode SCALE result data 252 | updated_obj = self.runtime_config.create_scale_object( 253 | type_string=change_scale_type, 254 | data=data, 255 | metadata=self.metadata 256 | ) 257 | updated_obj.decode() 258 | updated_obj.meta_info = {'result_found': result_found} 259 | 260 | return updated_obj 261 | 262 | def __repr__(self): 263 | if self.pallet and self.storage_function: 264 | return f'' 265 | elif self.data: 266 | return f'' 267 | else: 268 | return repr(self) 269 | -------------------------------------------------------------------------------- /substrateinterface/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import re 17 | 18 | 19 | def version_tuple(version_string: str) -> tuple: 20 | """ 21 | Converts a basic version string to a tuple that can be compared 22 | 23 | Parameters 24 | ---------- 25 | version_string 26 | 27 | Returns 28 | ------- 29 | tuple 30 | """ 31 | if re.search(r'[^\.0-9]', version_string): 32 | raise ValueError('version_string can only contain numeric characters') 33 | 34 | return tuple(int(v) for v in version_string.split('.')) 35 | -------------------------------------------------------------------------------- /substrateinterface/utils/caching.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2021 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from functools import lru_cache 18 | 19 | 20 | def block_dependent_lru_cache(maxsize=10, typed=False, block_arg_index=None): 21 | def decorator(f): 22 | cached_func = lru_cache(maxsize=maxsize, typed=typed)(f) 23 | 24 | def wrapper(*args, **kwargs): 25 | 26 | use_cache = False 27 | 28 | if block_arg_index is not None: 29 | if len(args) > block_arg_index and args[block_arg_index] is not None: 30 | use_cache = True 31 | 32 | if kwargs.get('block_hash') is not None or kwargs.get('block_id') is not None: 33 | use_cache = True 34 | 35 | if use_cache: 36 | return cached_func(*args, **kwargs) 37 | else: 38 | return f(*args, **kwargs) 39 | 40 | return wrapper 41 | return decorator 42 | -------------------------------------------------------------------------------- /substrateinterface/utils/ecdsa_helpers.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2021 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import hashlib 18 | import hmac 19 | import struct 20 | 21 | from ecdsa.curves import SECP256k1 22 | from eth_keys.datatypes import Signature, PrivateKey 23 | from eth_utils import to_checksum_address, keccak as eth_utils_keccak 24 | 25 | BIP39_PBKDF2_ROUNDS = 2048 26 | BIP39_SALT_MODIFIER = "mnemonic" 27 | BIP32_PRIVDEV = 0x80000000 28 | BIP32_CURVE = SECP256k1 29 | BIP32_SEED_MODIFIER = b'Bitcoin seed' 30 | ETH_DERIVATION_PATH = "m/44'/60'/0'/0" 31 | 32 | 33 | class PublicKey: 34 | def __init__(self, private_key): 35 | self.point = int.from_bytes(private_key, byteorder='big') * BIP32_CURVE.generator 36 | 37 | def __bytes__(self): 38 | xstr = int(self.point.x()).to_bytes(32, byteorder='big') 39 | parity = int(self.point.y()) & 1 40 | return (2 + parity).to_bytes(1, byteorder='big') + xstr 41 | 42 | def address(self): 43 | x = int(self.point.x()) 44 | y = int(self.point.y()) 45 | s = x.to_bytes(32, 'big') + y.to_bytes(32, 'big') 46 | return to_checksum_address(eth_utils_keccak(s)[12:]) 47 | 48 | 49 | def mnemonic_to_bip39seed(mnemonic, passphrase): 50 | mnemonic = bytes(mnemonic, 'utf8') 51 | salt = bytes(BIP39_SALT_MODIFIER + passphrase, 'utf8') 52 | return hashlib.pbkdf2_hmac('sha512', mnemonic, salt, BIP39_PBKDF2_ROUNDS) 53 | 54 | 55 | def bip39seed_to_bip32masternode(seed): 56 | h = hmac.new(BIP32_SEED_MODIFIER, seed, hashlib.sha512).digest() 57 | key, chain_code = h[:32], h[32:] 58 | return key, chain_code 59 | 60 | 61 | def derive_bip32childkey(parent_key, parent_chain_code, i): 62 | assert len(parent_key) == 32 63 | assert len(parent_chain_code) == 32 64 | k = parent_chain_code 65 | if (i & BIP32_PRIVDEV) != 0: 66 | key = b'\x00' + parent_key 67 | else: 68 | key = bytes(PublicKey(parent_key)) 69 | d = key + struct.pack('>L', i) 70 | while True: 71 | h = hmac.new(k, d, hashlib.sha512).digest() 72 | key, chain_code = h[:32], h[32:] 73 | a = int.from_bytes(key, byteorder='big') 74 | b = int.from_bytes(parent_key, byteorder='big') 75 | key = (a + b) % int(BIP32_CURVE.order) 76 | if a < BIP32_CURVE.order and key != 0: 77 | key = key.to_bytes(32, byteorder='big') 78 | break 79 | d = b'\x01' + h[32:] + struct.pack('>L', i) 80 | return key, chain_code 81 | 82 | 83 | def parse_derivation_path(str_derivation_path): 84 | path = [] 85 | if str_derivation_path[0:2] != 'm/': 86 | raise ValueError("Can't recognize derivation path. It should look like \"m/44'/60/0'/0\".") 87 | for i in str_derivation_path.lstrip('m/').split('/'): 88 | if "'" in i: 89 | path.append(BIP32_PRIVDEV + int(i[:-1])) 90 | else: 91 | path.append(int(i)) 92 | return path 93 | 94 | 95 | def mnemonic_to_ecdsa_private_key(mnemonic: str, str_derivation_path: str = None, passphrase: str = "") -> bytes: 96 | 97 | if str_derivation_path is None: 98 | str_derivation_path = f'{ETH_DERIVATION_PATH}/0' 99 | 100 | derivation_path = parse_derivation_path(str_derivation_path) 101 | bip39seed = mnemonic_to_bip39seed(mnemonic, passphrase) 102 | master_private_key, master_chain_code = bip39seed_to_bip32masternode(bip39seed) 103 | private_key, chain_code = master_private_key, master_chain_code 104 | for i in derivation_path: 105 | private_key, chain_code = derive_bip32childkey(private_key, chain_code, i) 106 | return private_key 107 | 108 | 109 | def ecdsa_sign(private_key: bytes, message: bytes) -> bytes: 110 | signer = PrivateKey(private_key) 111 | return signer.sign_msg(message).to_bytes() 112 | 113 | 114 | def ecdsa_verify(signature: bytes, data: bytes, address: bytes) -> bool: 115 | signature_obj = Signature(signature) 116 | recovered_pubkey = signature_obj.recover_public_key_from_msg(data) 117 | return recovered_pubkey.to_canonical_address() == address 118 | -------------------------------------------------------------------------------- /substrateinterface/utils/encrypted_json.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | from os import urandom 4 | 5 | from typing import Union 6 | 7 | from nacl.hashlib import scrypt 8 | from nacl.secret import SecretBox 9 | from sr25519 import pair_from_ed25519_secret_key 10 | 11 | 12 | NONCE_LENGTH = 24 13 | SCRYPT_LENGTH = 32 + (3 * 4) 14 | PKCS8_DIVIDER = bytes([161, 35, 3, 33, 0]) 15 | PKCS8_HEADER = bytes([48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]) 16 | PUB_LENGTH = 32 17 | SALT_LENGTH = 32 18 | SEC_LENGTH = 64 19 | SEED_LENGTH = 32 20 | 21 | SCRYPT_N = 1 << 15 22 | SCRYPT_P = 1 23 | SCRYPT_R = 8 24 | 25 | 26 | def decode_pair_from_encrypted_json(json_data: Union[str, dict], passphrase: str) -> tuple: 27 | """ 28 | Decodes encrypted PKCS#8 message from PolkadotJS JSON format 29 | 30 | Parameters 31 | ---------- 32 | json_data 33 | passphrase 34 | 35 | Returns 36 | ------- 37 | tuple containing private and public key 38 | """ 39 | if type(json_data) is str: 40 | json_data = json.loads(json_data) 41 | 42 | # Check requirements 43 | if json_data.get('encoding', {}).get('version') != "3": 44 | raise ValueError("Unsupported JSON format") 45 | 46 | encrypted = base64.b64decode(json_data['encoded']) 47 | 48 | if 'scrypt' in json_data['encoding']['type']: 49 | salt = encrypted[0:32] 50 | n = int.from_bytes(encrypted[32:36], byteorder='little') 51 | p = int.from_bytes(encrypted[36:40], byteorder='little') 52 | r = int.from_bytes(encrypted[40:44], byteorder='little') 53 | 54 | password = scrypt(passphrase.encode(), salt, n=n, r=r, p=p, dklen=32, maxmem=2 ** 26) 55 | encrypted = encrypted[SCRYPT_LENGTH:] 56 | 57 | else: 58 | password = passphrase.encode().rjust(32, b'\x00') 59 | 60 | if "xsalsa20-poly1305" not in json_data['encoding']['type']: 61 | raise ValueError("Unsupported encoding type") 62 | 63 | nonce = encrypted[0:NONCE_LENGTH] 64 | message = encrypted[NONCE_LENGTH:] 65 | 66 | secret_box = SecretBox(key=password) 67 | decrypted = secret_box.decrypt(message, nonce) 68 | 69 | # Decode PKCS8 message 70 | secret_key, public_key = decode_pkcs8(decrypted) 71 | 72 | if 'sr25519' in json_data['encoding']['content']: 73 | # Secret key from PolkadotJS is an Ed25519 expanded secret key, so has to be converted 74 | # https://github.com/polkadot-js/wasm/blob/master/packages/wasm-crypto/src/rs/sr25519.rs#L125 75 | converted_public_key, secret_key = pair_from_ed25519_secret_key(secret_key) 76 | assert(public_key == converted_public_key) 77 | 78 | return secret_key, public_key 79 | 80 | 81 | def decode_pkcs8(ciphertext: bytes) -> tuple: 82 | current_offset = 0 83 | 84 | header = ciphertext[current_offset:len(PKCS8_HEADER)] 85 | if header != PKCS8_HEADER: 86 | raise ValueError("Invalid Pkcs8 header found in body") 87 | 88 | current_offset += len(PKCS8_HEADER) 89 | 90 | secret_key = ciphertext[current_offset:current_offset + SEC_LENGTH] 91 | current_offset += SEC_LENGTH 92 | 93 | divider = ciphertext[current_offset:current_offset + len(PKCS8_DIVIDER)] 94 | 95 | if divider != PKCS8_DIVIDER: 96 | raise ValueError("Invalid Pkcs8 divider found in body") 97 | 98 | current_offset += len(PKCS8_DIVIDER) 99 | 100 | public_key = ciphertext[current_offset: current_offset + PUB_LENGTH] 101 | 102 | return secret_key, public_key 103 | 104 | 105 | def encode_pkcs8(public_key: bytes, private_key: bytes) -> bytes: 106 | return PKCS8_HEADER + private_key + PKCS8_DIVIDER + public_key 107 | 108 | 109 | def encode_pair(public_key: bytes, private_key: bytes, passphrase: str) -> bytes: 110 | """ 111 | Encode a public/private pair to PKCS#8 format, encrypted with provided passphrase 112 | 113 | Parameters 114 | ---------- 115 | public_key: 32 bytes public key 116 | private_key: 64 bytes private key 117 | passphrase: passphrase to encrypt the PKCS#8 message 118 | 119 | Returns 120 | ------- 121 | (Encrypted) PKCS#8 message bytes 122 | """ 123 | message = encode_pkcs8(public_key, private_key) 124 | 125 | salt = urandom(SALT_LENGTH) 126 | password = scrypt(passphrase.encode(), salt, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P, dklen=32, maxmem=2 ** 26) 127 | 128 | secret_box = SecretBox(key=password) 129 | message = secret_box.encrypt(message) 130 | 131 | scrypt_params = SCRYPT_N.to_bytes(4, 'little') + SCRYPT_P.to_bytes(4, 'little') + SCRYPT_R.to_bytes(4, 'little') 132 | 133 | return salt + scrypt_params + message.nonce + message.ciphertext 134 | 135 | -------------------------------------------------------------------------------- /substrateinterface/utils/hasher.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ Helper functions used to calculate keys for Substrate storage items 18 | """ 19 | 20 | from hashlib import blake2b 21 | import xxhash 22 | 23 | 24 | def blake2_256(data): 25 | """ 26 | Helper function to calculate a 32 bytes Blake2b hash for provided data, used as key for Substrate storage items 27 | 28 | Parameters 29 | ---------- 30 | data 31 | 32 | Returns 33 | ------- 34 | 35 | """ 36 | return blake2b(data, digest_size=32).digest() 37 | 38 | 39 | def blake2_128(data): 40 | """ 41 | Helper function to calculate a 16 bytes Blake2b hash for provided data, used as key for Substrate storage items 42 | 43 | Parameters 44 | ---------- 45 | data 46 | 47 | Returns 48 | ------- 49 | 50 | """ 51 | return blake2b(data, digest_size=16).digest() 52 | 53 | 54 | def blake2_128_concat(data): 55 | """ 56 | Helper function to calculate a 16 bytes Blake2b hash for provided data, concatenated with data, used as key 57 | for Substrate storage items 58 | 59 | Parameters 60 | ---------- 61 | data 62 | 63 | Returns 64 | ------- 65 | 66 | """ 67 | return blake2b(data, digest_size=16).digest() + data 68 | 69 | 70 | def xxh128(data): 71 | """ 72 | Helper function to calculate a 2 concatenated xxh64 hash for provided data, used as key for several Substrate 73 | 74 | Parameters 75 | ---------- 76 | data 77 | 78 | Returns 79 | ------- 80 | 81 | """ 82 | storage_key1 = bytearray(xxhash.xxh64(data, seed=0).digest()) 83 | storage_key1.reverse() 84 | 85 | storage_key2 = bytearray(xxhash.xxh64(data, seed=1).digest()) 86 | storage_key2.reverse() 87 | 88 | return storage_key1 + storage_key2 89 | 90 | 91 | def two_x64_concat(data): 92 | """ 93 | Helper function to calculate a xxh64 hash with concatenated data for provided data, 94 | used as key for several Substrate 95 | 96 | Parameters 97 | ---------- 98 | data 99 | 100 | Returns 101 | ------- 102 | 103 | """ 104 | storage_key = bytearray(xxhash.xxh64(data, seed=0).digest()) 105 | storage_key.reverse() 106 | 107 | return storage_key + data 108 | 109 | 110 | def xxh64(data): 111 | storage_key = bytearray(xxhash.xxh64(data, seed=0).digest()) 112 | storage_key.reverse() 113 | 114 | return storage_key 115 | 116 | 117 | def identity(data): 118 | return data 119 | -------------------------------------------------------------------------------- /substrateinterface/utils/ss58.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2021 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ss58.py 18 | 19 | """ SS58 is a simple address format designed for Substrate based chains. 20 | Encoding/decoding according to specification on 21 | https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58) 22 | 23 | """ 24 | from scalecodec.utils.ss58 import ss58_decode, ss58_encode, ss58_decode_account_index, ss58_encode_account_index, \ 25 | is_valid_ss58_address, get_ss58_format 26 | 27 | 28 | ss58_decode = ss58_decode 29 | ss58_encode = ss58_encode 30 | ss58_decode_account_index = ss58_decode_account_index 31 | ss58_encode_account_index = ss58_encode_account_index 32 | is_valid_ss58_address = is_valid_ss58_address 33 | get_ss58_format = get_ss58_format 34 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | -------------------------------------------------------------------------------- /test/fixtures/flipper-v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0xf051c631190ac47f82e280ba763df932210f6e2447978e24cbe0dcc6d6903c7a", 4 | "language": "ink! 4.0.0", 5 | "compiler": "rustc 1.63.0" 6 | }, 7 | "contract": { 8 | "name": "flipper", 9 | "version": "4.0.0", 10 | "authors": [ 11 | "Parity Technologies " 12 | ] 13 | }, 14 | "V3": { 15 | "spec": { 16 | "constructors": [ 17 | { 18 | "args": [ 19 | { 20 | "label": "init_value", 21 | "type": { 22 | "displayName": [ 23 | "bool" 24 | ], 25 | "type": 0 26 | } 27 | } 28 | ], 29 | "docs": [ 30 | "Creates a new flipper smart contract initialized with the given value." 31 | ], 32 | "label": "new", 33 | "payable": false, 34 | "selector": "0x9bae9d5e" 35 | }, 36 | { 37 | "args": [], 38 | "docs": [ 39 | "Creates a new flipper smart contract initialized to `false`." 40 | ], 41 | "label": "default", 42 | "payable": false, 43 | "selector": "0xed4b9d1b" 44 | } 45 | ], 46 | "docs": [], 47 | "events": [], 48 | "messages": [ 49 | { 50 | "args": [], 51 | "docs": [ 52 | " Flips the current value of the Flipper's boolean." 53 | ], 54 | "label": "flip", 55 | "mutates": true, 56 | "payable": false, 57 | "returnType": null, 58 | "selector": "0x633aa551" 59 | }, 60 | { 61 | "args": [], 62 | "docs": [ 63 | " Returns the current value of the Flipper's boolean." 64 | ], 65 | "label": "get", 66 | "mutates": false, 67 | "payable": false, 68 | "returnType": { 69 | "displayName": [ 70 | "bool" 71 | ], 72 | "type": 0 73 | }, 74 | "selector": "0x2f865bd9" 75 | } 76 | ] 77 | }, 78 | "storage": { 79 | "struct": { 80 | "fields": [ 81 | { 82 | "layout": { 83 | "cell": { 84 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000", 85 | "ty": 0 86 | } 87 | }, 88 | "name": "value" 89 | } 90 | ] 91 | } 92 | }, 93 | "types": [ 94 | { 95 | "id": 0, 96 | "type": { 97 | "def": { 98 | "primitive": "bool" 99 | } 100 | } 101 | } 102 | ] 103 | } 104 | } -------------------------------------------------------------------------------- /test/fixtures/flipper-v4.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0xf051c631190ac47f82e280ba763df932210f6e2447978e24cbe0dcc6d6903c7a", 4 | "language": "ink! 4.0.0-alpha.1", 5 | "compiler": "rustc 1.63.0" 6 | }, 7 | "contract": { 8 | "name": "flipper", 9 | "version": "4.0.0-alpha.1", 10 | "authors": [ 11 | "Parity Technologies " 12 | ] 13 | }, 14 | "spec": { 15 | "constructors": [ 16 | { 17 | "args": [ 18 | { 19 | "label": "init_value", 20 | "type": { 21 | "displayName": [ 22 | "bool" 23 | ], 24 | "type": 0 25 | } 26 | } 27 | ], 28 | "docs": [ 29 | "Creates a new flipper smart contract initialized with the given value." 30 | ], 31 | "label": "new", 32 | "payable": false, 33 | "selector": "0x9bae9d5e" 34 | }, 35 | { 36 | "args": [], 37 | "docs": [ 38 | "Creates a new flipper smart contract initialized to `false`." 39 | ], 40 | "label": "default", 41 | "payable": false, 42 | "selector": "0xed4b9d1b" 43 | } 44 | ], 45 | "docs": [], 46 | "events": [], 47 | "messages": [ 48 | { 49 | "args": [], 50 | "docs": [ 51 | " Flips the current value of the Flipper's boolean." 52 | ], 53 | "label": "flip", 54 | "mutates": true, 55 | "payable": false, 56 | "returnType": null, 57 | "selector": "0x633aa551" 58 | }, 59 | { 60 | "args": [], 61 | "docs": [ 62 | " Returns the current value of the Flipper's boolean." 63 | ], 64 | "label": "get", 65 | "mutates": false, 66 | "payable": false, 67 | "returnType": { 68 | "displayName": [ 69 | "bool" 70 | ], 71 | "type": 0 72 | }, 73 | "selector": "0x2f865bd9" 74 | } 75 | ] 76 | }, 77 | "storage": { 78 | "struct": { 79 | "fields": [ 80 | { 81 | "layout": { 82 | "cell": { 83 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000", 84 | "ty": 0 85 | } 86 | }, 87 | "name": "value" 88 | } 89 | ] 90 | } 91 | }, 92 | "types": [ 93 | { 94 | "id": 0, 95 | "type": { 96 | "def": { 97 | "primitive": "bool" 98 | } 99 | } 100 | } 101 | ], 102 | "version": "4" 103 | } -------------------------------------------------------------------------------- /test/fixtures/flipper-v5.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0x04208d5b3de1808ed1ccf83d08bdb5e3974bba7c4af8d5fc1ed6d6c725155c2e", 4 | "language": "ink! 5.0.0", 5 | "compiler": "rustc 1.81.0-nightly", 6 | "build_info": { 7 | "build_mode": "Debug", 8 | "cargo_contract_version": "4.1.1", 9 | "rust_toolchain": "nightly-aarch64-apple-darwin", 10 | "wasm_opt_settings": { 11 | "keep_debug_symbols": false, 12 | "optimization_passes": "Z" 13 | } 14 | } 15 | }, 16 | "contract": { 17 | "name": "flipper5", 18 | "version": "0.1.0", 19 | "authors": [ 20 | "[your_name] <[your_email]>" 21 | ] 22 | }, 23 | "image": null, 24 | "spec": { 25 | "constructors": [ 26 | { 27 | "args": [ 28 | { 29 | "label": "init_value", 30 | "type": { 31 | "displayName": [ 32 | "bool" 33 | ], 34 | "type": 0 35 | } 36 | } 37 | ], 38 | "default": false, 39 | "docs": [ 40 | "Constructor that initializes the `bool` value to the given `init_value`." 41 | ], 42 | "label": "new", 43 | "payable": false, 44 | "returnType": { 45 | "displayName": [ 46 | "ink_primitives", 47 | "ConstructorResult" 48 | ], 49 | "type": 2 50 | }, 51 | "selector": "0x9bae9d5e" 52 | }, 53 | { 54 | "args": [], 55 | "default": false, 56 | "docs": [ 57 | "Constructor that initializes the `bool` value to `false`.", 58 | "", 59 | "Constructors can delegate to other constructors." 60 | ], 61 | "label": "default", 62 | "payable": false, 63 | "returnType": { 64 | "displayName": [ 65 | "ink_primitives", 66 | "ConstructorResult" 67 | ], 68 | "type": 2 69 | }, 70 | "selector": "0xed4b9d1b" 71 | } 72 | ], 73 | "docs": [], 74 | "environment": { 75 | "accountId": { 76 | "displayName": [ 77 | "AccountId" 78 | ], 79 | "type": 7 80 | }, 81 | "balance": { 82 | "displayName": [ 83 | "Balance" 84 | ], 85 | "type": 9 86 | }, 87 | "blockNumber": { 88 | "displayName": [ 89 | "BlockNumber" 90 | ], 91 | "type": 12 92 | }, 93 | "chainExtension": { 94 | "displayName": [ 95 | "ChainExtension" 96 | ], 97 | "type": 13 98 | }, 99 | "hash": { 100 | "displayName": [ 101 | "Hash" 102 | ], 103 | "type": 10 104 | }, 105 | "maxEventTopics": 4, 106 | "staticBufferSize": 16384, 107 | "timestamp": { 108 | "displayName": [ 109 | "Timestamp" 110 | ], 111 | "type": 11 112 | } 113 | }, 114 | "events": [ 115 | { 116 | "args": [ 117 | { 118 | "docs": [], 119 | "indexed": true, 120 | "label": "value", 121 | "type": { 122 | "displayName": [ 123 | "bool" 124 | ], 125 | "type": 0 126 | } 127 | } 128 | ], 129 | "docs": [], 130 | "label": "Flipped", 131 | "module_path": "flipper5::flipper5", 132 | "signature_topic": "0x529cf346ddea0543633a1d91f021fa688fb7fe023ee1fb83ad031fe005673254" 133 | }, 134 | { 135 | "args": [ 136 | { 137 | "docs": [], 138 | "indexed": true, 139 | "label": "test_value", 140 | "type": { 141 | "displayName": [ 142 | "u8" 143 | ], 144 | "type": 6 145 | } 146 | } 147 | ], 148 | "docs": [], 149 | "label": "Test", 150 | "module_path": "flipper5::flipper5", 151 | "signature_topic": "0xc04204b5a8f12647ea7e92832f7608f4b5279fbcd2181333ff6a96906e5d555f" 152 | } 153 | ], 154 | "lang_error": { 155 | "displayName": [ 156 | "ink", 157 | "LangError" 158 | ], 159 | "type": 4 160 | }, 161 | "messages": [ 162 | { 163 | "args": [], 164 | "default": false, 165 | "docs": [ 166 | " A message that can be called on instantiated contracts.", 167 | " This one flips the value of the stored `bool` from `true`", 168 | " to `false` and vice versa." 169 | ], 170 | "label": "flip", 171 | "mutates": true, 172 | "payable": false, 173 | "returnType": { 174 | "displayName": [ 175 | "ink", 176 | "MessageResult" 177 | ], 178 | "type": 2 179 | }, 180 | "selector": "0x633aa551" 181 | }, 182 | { 183 | "args": [], 184 | "default": false, 185 | "docs": [ 186 | " Simply returns the current value of our `bool`." 187 | ], 188 | "label": "get", 189 | "mutates": false, 190 | "payable": false, 191 | "returnType": { 192 | "displayName": [ 193 | "ink", 194 | "MessageResult" 195 | ], 196 | "type": 5 197 | }, 198 | "selector": "0x2f865bd9" 199 | } 200 | ] 201 | }, 202 | "storage": { 203 | "root": { 204 | "layout": { 205 | "struct": { 206 | "fields": [ 207 | { 208 | "layout": { 209 | "leaf": { 210 | "key": "0x00000000", 211 | "ty": 0 212 | } 213 | }, 214 | "name": "value" 215 | } 216 | ], 217 | "name": "Flipper5" 218 | } 219 | }, 220 | "root_key": "0x00000000", 221 | "ty": 1 222 | } 223 | }, 224 | "types": [ 225 | { 226 | "id": 0, 227 | "type": { 228 | "def": { 229 | "primitive": "bool" 230 | } 231 | } 232 | }, 233 | { 234 | "id": 1, 235 | "type": { 236 | "def": { 237 | "composite": { 238 | "fields": [ 239 | { 240 | "name": "value", 241 | "type": 0, 242 | "typeName": ",>>::Type" 243 | } 244 | ] 245 | } 246 | }, 247 | "path": [ 248 | "flipper5", 249 | "flipper5", 250 | "Flipper5" 251 | ] 252 | } 253 | }, 254 | { 255 | "id": 2, 256 | "type": { 257 | "def": { 258 | "variant": { 259 | "variants": [ 260 | { 261 | "fields": [ 262 | { 263 | "type": 3 264 | } 265 | ], 266 | "index": 0, 267 | "name": "Ok" 268 | }, 269 | { 270 | "fields": [ 271 | { 272 | "type": 4 273 | } 274 | ], 275 | "index": 1, 276 | "name": "Err" 277 | } 278 | ] 279 | } 280 | }, 281 | "params": [ 282 | { 283 | "name": "T", 284 | "type": 3 285 | }, 286 | { 287 | "name": "E", 288 | "type": 4 289 | } 290 | ], 291 | "path": [ 292 | "Result" 293 | ] 294 | } 295 | }, 296 | { 297 | "id": 3, 298 | "type": { 299 | "def": { 300 | "tuple": [] 301 | } 302 | } 303 | }, 304 | { 305 | "id": 4, 306 | "type": { 307 | "def": { 308 | "variant": { 309 | "variants": [ 310 | { 311 | "index": 1, 312 | "name": "CouldNotReadInput" 313 | } 314 | ] 315 | } 316 | }, 317 | "path": [ 318 | "ink_primitives", 319 | "LangError" 320 | ] 321 | } 322 | }, 323 | { 324 | "id": 5, 325 | "type": { 326 | "def": { 327 | "variant": { 328 | "variants": [ 329 | { 330 | "fields": [ 331 | { 332 | "type": 0 333 | } 334 | ], 335 | "index": 0, 336 | "name": "Ok" 337 | }, 338 | { 339 | "fields": [ 340 | { 341 | "type": 4 342 | } 343 | ], 344 | "index": 1, 345 | "name": "Err" 346 | } 347 | ] 348 | } 349 | }, 350 | "params": [ 351 | { 352 | "name": "T", 353 | "type": 0 354 | }, 355 | { 356 | "name": "E", 357 | "type": 4 358 | } 359 | ], 360 | "path": [ 361 | "Result" 362 | ] 363 | } 364 | }, 365 | { 366 | "id": 6, 367 | "type": { 368 | "def": { 369 | "primitive": "u8" 370 | } 371 | } 372 | }, 373 | { 374 | "id": 7, 375 | "type": { 376 | "def": { 377 | "composite": { 378 | "fields": [ 379 | { 380 | "type": 8, 381 | "typeName": "[u8; 32]" 382 | } 383 | ] 384 | } 385 | }, 386 | "path": [ 387 | "ink_primitives", 388 | "types", 389 | "AccountId" 390 | ] 391 | } 392 | }, 393 | { 394 | "id": 8, 395 | "type": { 396 | "def": { 397 | "array": { 398 | "len": 32, 399 | "type": 6 400 | } 401 | } 402 | } 403 | }, 404 | { 405 | "id": 9, 406 | "type": { 407 | "def": { 408 | "primitive": "u128" 409 | } 410 | } 411 | }, 412 | { 413 | "id": 10, 414 | "type": { 415 | "def": { 416 | "composite": { 417 | "fields": [ 418 | { 419 | "type": 8, 420 | "typeName": "[u8; 32]" 421 | } 422 | ] 423 | } 424 | }, 425 | "path": [ 426 | "ink_primitives", 427 | "types", 428 | "Hash" 429 | ] 430 | } 431 | }, 432 | { 433 | "id": 11, 434 | "type": { 435 | "def": { 436 | "primitive": "u64" 437 | } 438 | } 439 | }, 440 | { 441 | "id": 12, 442 | "type": { 443 | "def": { 444 | "primitive": "u32" 445 | } 446 | } 447 | }, 448 | { 449 | "id": 13, 450 | "type": { 451 | "def": { 452 | "variant": {} 453 | }, 454 | "path": [ 455 | "ink_env", 456 | "types", 457 | "NoChainExtension" 458 | ] 459 | } 460 | } 461 | ], 462 | "version": 5 463 | } -------------------------------------------------------------------------------- /test/fixtures/incorrect_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "random": "data" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/polkadotjs_encrypted.json: -------------------------------------------------------------------------------- 1 | {"encoded":"sqM3qhvUpJ/Q40FUR0dBrv1a0HfElvAAyU0dSd9L1okAgAAAAQAAAAgAAACVc/7PcVGSC/SBsTb/5V1irTu4nVWSRFQrW9nRAHIIsNC7O40tyRdb8Pz+gqYvBa/K9xYx5IHN5O6NZ/DxsEaDBDvieITrPlK0LLirsQGSYV+5KekMmqy6L/YAVcPW4XE7KPNS52tCpT3POaG8VNhhmws+WmVpJYfeVnhg21K0Rta23pF3h8K90aSM1SMA7QyoXinBngsQqyPaoS+p","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"address":"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY","meta":{"genesisHash":"0x6408de7737c59c238890533af25896a2c20608d8b380bb01029acb392781063e","isHardware":false,"name":"alice test","tags":[],"whenCreated":1666779395081}} 2 | -------------------------------------------------------------------------------- /test/fixtures/polkadotjs_encrypted_ed25519.json: -------------------------------------------------------------------------------- 1 | {"encoded":"5xHiGDnqsR1zCNn5Jc+Iqtby0EPEPrLaZ8u1cZ3N0kgAgAAAAQAAAAgAAABWwu3LswRvYqi7B+CterIfJ0fP13fHEImvR2ngX1fbFkZHZLysu6f+Wh8O/dF22FAOiROANEhwppX8Vd+xcRdzup8ujvSjeFTnKyuLKvpZEquyLJ27DIsXZeuDlPFN/A03MLbI206N3r6wblc7L6CL+qVP1AOI2wkiO4qT/J/LEFQT0ets7DGM3Sm61XpSMXrKxd4TSQ03ceGOlrmB","encoding":{"content":["pkcs8","ed25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"address":"5FxjTxVWebYJeoPZ9H6XHkYVxvS5j7MN5jpJwWq7F9Pidz3K","meta":{"isHardware":false,"name":"ed_pkscp test","tags":[],"whenCreated":1666622747763}} -------------------------------------------------------------------------------- /test/settings.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | from os import environ 17 | 18 | KUSAMA_NODE_URL = environ.get('SUBSTRATE_NODE_URL_KUSAMA') or 'wss://kusama-rpc.polkadot.io/' 19 | POLKADOT_NODE_URL = environ.get('SUBSTRATE_NODE_URL_POLKADOT') or 'wss://rpc.polkadot.io/' 20 | ROCOCO_NODE_URL = environ.get('SUBSTRATE_NODE_URL_ROCOCO') or 'wss://rococo-rpc.polkadot.io' 21 | MOONBEAM_NODE_URL = environ.get('SUBSTRATE_NODE_URL_ROCOCO') or 'wss://wss.api.moonbeam.network' 22 | 23 | BABE_NODE_URL = environ.get('SUBSTRATE_BABE_NODE_URL') or POLKADOT_NODE_URL 24 | AURA_NODE_URL = environ.get('SUBSTRATE_AURA_NODE_URL') or 'wss://acala-rpc-1.aca-api.network' 25 | 26 | -------------------------------------------------------------------------------- /test/test_extension_interface.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2023 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from datetime import datetime, timezone 18 | 19 | import unittest 20 | 21 | from substrateinterface import SubstrateInterface 22 | from substrateinterface.exceptions import ExtensionCallNotFound 23 | from substrateinterface.extensions import SubstrateNodeExtension 24 | from test import settings 25 | 26 | 27 | class ExtensionsTestCase(unittest.TestCase): 28 | 29 | @classmethod 30 | def setUpClass(cls): 31 | cls.substrate = SubstrateInterface( 32 | url=settings.POLKADOT_NODE_URL 33 | ) 34 | cls.substrate.register_extension(SubstrateNodeExtension(max_block_range=100)) 35 | 36 | def test_search_block_number(self): 37 | block_datetime = datetime(2020, 7, 12, 0, 0, 0, tzinfo=timezone.utc) 38 | 39 | block_number = self.substrate.extensions.search_block_number(block_datetime=block_datetime) 40 | 41 | self.assertGreaterEqual(block_number, 665270) 42 | self.assertLessEqual(block_number, 665280) 43 | 44 | def test_search_block_timestamp(self): 45 | block_timestamp = self.substrate.extensions.get_block_timestamp(1000) 46 | self.assertEqual(1590513426, block_timestamp) 47 | 48 | def test_unsupported_extension_call(self): 49 | with self.assertRaises(ExtensionCallNotFound): 50 | self.substrate.extensions.unknown() 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /test/test_init.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | from scalecodec import ScaleBytes 20 | from scalecodec.exceptions import RemainingScaleBytesNotEmptyException 21 | from substrateinterface import SubstrateInterface 22 | from test import settings 23 | 24 | 25 | class TestInit(unittest.TestCase): 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | cls.kusama_substrate = SubstrateInterface(url=settings.KUSAMA_NODE_URL) 30 | cls.polkadot_substrate = SubstrateInterface(url=settings.POLKADOT_NODE_URL) 31 | 32 | def test_chain(self): 33 | self.assertEqual('Kusama', self.kusama_substrate.chain) 34 | self.assertEqual('Polkadot', self.polkadot_substrate.chain) 35 | 36 | def test_properties(self): 37 | self.assertDictEqual( 38 | {'ss58Format': 2, 'tokenDecimals': 12, 'tokenSymbol': 'KSM'}, self.kusama_substrate.properties 39 | ) 40 | self.assertDictEqual( 41 | {'ss58Format': 0, 'tokenDecimals': 10, 'tokenSymbol': 'DOT'}, self.polkadot_substrate.properties 42 | ) 43 | 44 | def test_ss58_format(self): 45 | self.assertEqual(2, self.kusama_substrate.ss58_format) 46 | self.assertEqual(0, self.polkadot_substrate.ss58_format) 47 | 48 | def test_token_symbol(self): 49 | self.assertEqual('KSM', self.kusama_substrate.token_symbol) 50 | self.assertEqual('DOT', self.polkadot_substrate.token_symbol) 51 | 52 | def test_token_decimals(self): 53 | self.assertEqual(12, self.kusama_substrate.token_decimals) 54 | self.assertEqual(10, self.polkadot_substrate.token_decimals) 55 | 56 | def test_override_ss58_format_init(self): 57 | substrate = SubstrateInterface(url=settings.KUSAMA_NODE_URL, ss58_format=99) 58 | self.assertEqual(99, substrate.ss58_format) 59 | 60 | def test_override_incorrect_ss58_format(self): 61 | substrate = SubstrateInterface(url=settings.KUSAMA_NODE_URL) 62 | with self.assertRaises(TypeError): 63 | substrate.ss58_format = 'test' 64 | 65 | def test_override_token_symbol(self): 66 | substrate = SubstrateInterface(url=settings.KUSAMA_NODE_URL) 67 | substrate.token_symbol = 'TST' 68 | self.assertEqual('TST', substrate.token_symbol) 69 | 70 | def test_override_incorrect_token_decimals(self): 71 | substrate = SubstrateInterface(url=settings.KUSAMA_NODE_URL) 72 | with self.assertRaises(TypeError): 73 | substrate.token_decimals = 'test' 74 | 75 | def test_is_valid_ss58_address(self): 76 | self.assertTrue(self.kusama_substrate.is_valid_ss58_address('GLdQ4D4wkeEJUX8DBT9HkpycFVYQZ3fmJyQ5ZgBRxZ4LD3S')) 77 | self.assertFalse( 78 | self.kusama_substrate.is_valid_ss58_address('12gX42C4Fj1wgtfgoP624zeHrcPBqzhb4yAENyvFdGX6EUnN') 79 | ) 80 | self.assertFalse( 81 | self.kusama_substrate.is_valid_ss58_address('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 82 | ) 83 | 84 | self.assertFalse(self.polkadot_substrate.is_valid_ss58_address('GLdQ4D4wkeEJUX8DBT9HkpycFVYQZ3fmJyQ5ZgBRxZ4LD3S')) 85 | self.assertTrue( 86 | self.polkadot_substrate.is_valid_ss58_address('12gX42C4Fj1wgtfgoP624zeHrcPBqzhb4yAENyvFdGX6EUnN') 87 | ) 88 | 89 | def test_lru_cache_not_shared(self): 90 | block_number = self.kusama_substrate.get_block_number("0xa4d873095aeae6fc1f3953f0a0085ee216bf8629342aaa92bd53f841e1052e1c") 91 | block_number2 = self.polkadot_substrate.get_block_number( 92 | "0xa4d873095aeae6fc1f3953f0a0085ee216bf8629342aaa92bd53f841e1052e1c") 93 | 94 | self.assertIsNotNone(block_number) 95 | self.assertIsNone(block_number2) 96 | 97 | def test_context_manager(self): 98 | with SubstrateInterface(url=settings.KUSAMA_NODE_URL) as substrate: 99 | self.assertTrue(substrate.websocket.connected) 100 | self.assertEqual(2, substrate.ss58_format) 101 | 102 | self.assertFalse(substrate.websocket.connected) 103 | 104 | def test_strict_scale_decode(self): 105 | 106 | with self.assertRaises(RemainingScaleBytesNotEmptyException): 107 | self.kusama_substrate.decode_scale('u8', ScaleBytes('0x0101')) 108 | 109 | with SubstrateInterface(url=settings.KUSAMA_NODE_URL, config={'strict_scale_decode': False}) as substrate: 110 | result = substrate.decode_scale('u8', ScaleBytes('0x0101')) 111 | self.assertEqual(result, 1) 112 | 113 | 114 | if __name__ == '__main__': 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /test/test_query.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | from unittest.mock import MagicMock 19 | 20 | from substrateinterface import SubstrateInterface 21 | from substrateinterface.exceptions import StorageFunctionNotFound 22 | from test import settings 23 | 24 | 25 | class QueryTestCase(unittest.TestCase): 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | cls.kusama_substrate = SubstrateInterface( 30 | url=settings.KUSAMA_NODE_URL, 31 | ss58_format=2, 32 | type_registry_preset='kusama' 33 | ) 34 | 35 | cls.polkadot_substrate = SubstrateInterface( 36 | url=settings.POLKADOT_NODE_URL, 37 | ss58_format=0, 38 | type_registry_preset='polkadot' 39 | ) 40 | 41 | def test_system_account(self): 42 | 43 | result = self.kusama_substrate.query( 44 | module='System', 45 | storage_function='Account', 46 | params=['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T'], 47 | block_hash='0xbf787e2f322080e137ed53e763b1cc97d5c5585be1f736914e27d68ac97f5f2c' 48 | ) 49 | 50 | self.assertEqual(67501, result.value['nonce']) 51 | self.assertEqual(1099945000512, result.value['data']['free']) 52 | self.assertEqual(result.meta_info['result_found'], True) 53 | 54 | def test_system_account_non_existing(self): 55 | result = self.kusama_substrate.query( 56 | module='System', 57 | storage_function='Account', 58 | params=['GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi'] 59 | ) 60 | 61 | self.assertEqual( 62 | { 63 | 'nonce': 0, 'consumers': 0, 'providers': 0, 'sufficients': 0, 64 | 'data': { 65 | 'free': 0, 'reserved': 0, 'frozen': 0, 'flags': 170141183460469231731687303715884105728 66 | } 67 | }, result.value) 68 | 69 | def test_non_existing_query(self): 70 | with self.assertRaises(StorageFunctionNotFound) as cm: 71 | self.kusama_substrate.query("Unknown", "StorageFunction") 72 | 73 | self.assertEqual('Pallet "Unknown" not found', str(cm.exception)) 74 | 75 | def test_missing_params(self): 76 | with self.assertRaises(ValueError) as cm: 77 | self.kusama_substrate.query("System", "Account") 78 | 79 | def test_modifier_default_result(self): 80 | result = self.kusama_substrate.query( 81 | module='Staking', 82 | storage_function='HistoryDepth', 83 | block_hash='0x4b313e72e3a524b98582c31cd3ff6f7f2ef5c38a3c899104a833e468bb1370a2' 84 | ) 85 | 86 | self.assertEqual(84, result.value) 87 | self.assertEqual(result.meta_info['result_found'], False) 88 | 89 | def test_modifier_option_result(self): 90 | 91 | result = self.kusama_substrate.query( 92 | module='Identity', 93 | storage_function='IdentityOf', 94 | params=["DD6kXYJPHbPRbBjeR35s1AR7zDh7W2aE55EBuDyMorQZS2a"], 95 | block_hash='0x4b313e72e3a524b98582c31cd3ff6f7f2ef5c38a3c899104a833e468bb1370a2' 96 | ) 97 | 98 | self.assertIsNone(result.value) 99 | self.assertEqual(result.meta_info['result_found'], False) 100 | 101 | def test_identity_hasher(self): 102 | result = self.kusama_substrate.query("Claims", "Claims", ["0x00000a9c44f24e314127af63ae55b864a28d7aee"]) 103 | self.assertEqual(45880000000000, result.value) 104 | 105 | def test_well_known_keys_result(self): 106 | result = self.kusama_substrate.query("Substrate", "Code") 107 | self.assertIsNotNone(result.value) 108 | 109 | def test_well_known_keys_default(self): 110 | result = self.kusama_substrate.query("Substrate", "HeapPages") 111 | self.assertEqual(0, result.value) 112 | 113 | def test_well_known_keys_not_found(self): 114 | with self.assertRaises(StorageFunctionNotFound): 115 | self.kusama_substrate.query("Substrate", "Unknown") 116 | 117 | def test_well_known_pallet_version(self): 118 | 119 | sf = self.kusama_substrate.get_metadata_storage_function("Balances", "PalletVersion") 120 | self.assertEqual(sf.value['name'], ':__STORAGE_VERSION__:') 121 | 122 | result = self.kusama_substrate.query("Balances", "PalletVersion") 123 | self.assertGreaterEqual(result.value, 1) 124 | 125 | def test_query_multi(self): 126 | 127 | storage_keys = [ 128 | self.kusama_substrate.create_storage_key( 129 | "System", "Account", ["F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T"] 130 | ), 131 | self.kusama_substrate.create_storage_key( 132 | "System", "Account", ["GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi"] 133 | ), 134 | self.kusama_substrate.create_storage_key( 135 | "Staking", "Bonded", ["GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi"] 136 | ) 137 | ] 138 | 139 | result = self.kusama_substrate.query_multi(storage_keys) 140 | 141 | self.assertEqual(len(result), 3) 142 | self.assertEqual(result[0][0].params[0], "F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T") 143 | self.assertGreater(result[0][1].value['nonce'], 0) 144 | self.assertEqual(result[1][1].value['nonce'], 0) 145 | 146 | def test_storage_key_unknown(self): 147 | with self.assertRaises(StorageFunctionNotFound): 148 | self.kusama_substrate.create_storage_key("Unknown", "Unknown") 149 | 150 | with self.assertRaises(StorageFunctionNotFound): 151 | self.kusama_substrate.create_storage_key("System", "Unknown") 152 | 153 | 154 | if __name__ == '__main__': 155 | unittest.main() 156 | -------------------------------------------------------------------------------- /test/test_query_map.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | from unittest.mock import MagicMock 19 | 20 | from scalecodec.types import GenericAccountId 21 | 22 | from substrateinterface.exceptions import SubstrateRequestException 23 | 24 | from substrateinterface import SubstrateInterface 25 | from test import settings 26 | 27 | 28 | class QueryMapTestCase(unittest.TestCase): 29 | 30 | @classmethod 31 | def setUpClass(cls): 32 | 33 | cls.kusama_substrate = SubstrateInterface( 34 | url=settings.KUSAMA_NODE_URL, 35 | ss58_format=2, 36 | type_registry_preset='kusama' 37 | ) 38 | 39 | orig_rpc_request = cls.kusama_substrate.rpc_request 40 | 41 | def mocked_request(method, params): 42 | if method == 'state_getKeysPaged': 43 | if params[3] == '0x2e8047826d028f5cc092f5e694860efbd4f74ee1535424cdf3626a175867db62': 44 | 45 | if params[2] == params[0]: 46 | return { 47 | 'jsonrpc': '2.0', 48 | 'result': [ 49 | '0x9c5d795d0297be56027a4b2464e333979c5d795d0297be56027a4b2464e3339700000a9c44f24e314127af63ae55b864a28d7aee', 50 | '0x9c5d795d0297be56027a4b2464e333979c5d795d0297be56027a4b2464e3339700002f21194993a750972574e2d82ce8c95078a6', 51 | '0x9c5d795d0297be56027a4b2464e333979c5d795d0297be56027a4b2464e333970000a940f973ccf435ae9c040c253e1c043c5fb2', 52 | '0x9c5d795d0297be56027a4b2464e333979c5d795d0297be56027a4b2464e3339700010b75619f666c3f172f0d1c7fa86d02adcf9c' 53 | ], 54 | 'id': 8 55 | } 56 | else: 57 | return { 58 | 'jsonrpc': '2.0', 59 | 'result': [ 60 | ], 61 | 'id': 8 62 | } 63 | return orig_rpc_request(method, params) 64 | 65 | cls.kusama_substrate.rpc_request = MagicMock(side_effect=mocked_request) 66 | 67 | def test_claims_claim_map(self): 68 | 69 | result = self.kusama_substrate.query_map('Claims', 'Claims', max_results=3) 70 | 71 | records = [item for item in result] 72 | 73 | self.assertEqual(3, len(records)) 74 | self.assertEqual(45880000000000, records[0][1].value) 75 | self.assertEqual('0x00000a9c44f24e314127af63ae55b864a28d7aee', records[0][0].value) 76 | self.assertEqual('0x00002f21194993a750972574e2d82ce8c95078a6', records[1][0].value) 77 | self.assertEqual('0x0000a940f973ccf435ae9c040c253e1c043c5fb2', records[2][0].value) 78 | 79 | def test_system_account_map_block_hash(self): 80 | 81 | # Retrieve first two records from System.Account query map 82 | 83 | result = self.kusama_substrate.query_map( 84 | 'System', 'Account', page_size=1, 85 | block_hash="0x587a1e69871c09f2408d724ceebbe16edc4a69139b5df9786e1057c4d041af73" 86 | ) 87 | 88 | record_1_1 = next(result) 89 | 90 | self.assertEqual(type(record_1_1[0]), GenericAccountId) 91 | self.assertIn('data', record_1_1[1].value) 92 | self.assertIn('nonce', record_1_1[1].value) 93 | 94 | # Next record set must trigger RPC call 95 | 96 | record_1_2 = next(result) 97 | 98 | self.assertEqual(type(record_1_2[0]), GenericAccountId) 99 | self.assertIn('data', record_1_2[1].value) 100 | self.assertIn('nonce', record_1_2[1].value) 101 | 102 | # Same query map with yield of 2 must result in same records 103 | 104 | result = self.kusama_substrate.query_map( 105 | 'System', 'Account', page_size=2, 106 | block_hash="0x587a1e69871c09f2408d724ceebbe16edc4a69139b5df9786e1057c4d041af73" 107 | ) 108 | 109 | record_2_1 = next(result) 110 | record_2_2 = next(result) 111 | 112 | self.assertEqual(record_1_1[0].value, record_2_1[0].value) 113 | self.assertEqual(record_1_1[1].value, record_2_1[1].value) 114 | self.assertEqual(record_1_2[0].value, record_2_2[0].value) 115 | self.assertEqual(record_1_2[1].value, record_2_2[1].value) 116 | 117 | def test_max_results(self): 118 | result = self.kusama_substrate.query_map('Claims', 'Claims', max_results=5, page_size=100) 119 | 120 | # Keep iterating shouldn't trigger retrieve next page 121 | result_count = 0 122 | for _ in result: 123 | result_count += 1 124 | 125 | self.assertEqual(5, result_count) 126 | 127 | result = self.kusama_substrate.query_map('Claims', 'Claims', max_results=5, page_size=2) 128 | 129 | # Keep iterating shouldn't exceed max_results 130 | result_count = 0 131 | for record in result: 132 | result_count += 1 133 | if result_count == 1: 134 | self.assertEqual('0x00000a9c44f24e314127af63ae55b864a28d7aee', record[0].value) 135 | elif result_count == 2: 136 | self.assertEqual('0x00002f21194993a750972574e2d82ce8c95078a6', record[0].value) 137 | elif result_count == 3: 138 | self.assertEqual('0x0000a940f973ccf435ae9c040c253e1c043c5fb2', record[0].value) 139 | 140 | self.assertEqual(5, result_count) 141 | 142 | def test_result_exhausted(self): 143 | result = self.kusama_substrate.query_map( 144 | module='Claims', storage_function='Claims', 145 | block_hash='0x2e8047826d028f5cc092f5e694860efbd4f74ee1535424cdf3626a175867db62' 146 | ) 147 | 148 | result_count = 0 149 | for _ in result: 150 | result_count += 1 151 | 152 | self.assertEqual(4, result_count) 153 | 154 | def test_non_existing_query_map(self): 155 | with self.assertRaises(ValueError) as cm: 156 | self.kusama_substrate.query_map("Unknown", "StorageFunction") 157 | 158 | self.assertEqual('Pallet "Unknown" not found', str(cm.exception)) 159 | 160 | def test_non_map_function_query_map(self): 161 | with self.assertRaises(ValueError) as cm: 162 | self.kusama_substrate.query_map("System", "Events") 163 | 164 | self.assertEqual('Given storage function is not a map', str(cm.exception)) 165 | 166 | def test_exceed_maximum_page_size(self): 167 | with self.assertRaises(SubstrateRequestException): 168 | self.kusama_substrate.query_map( 169 | 'System', 'Account', page_size=9999999 170 | ) 171 | 172 | def test_double_map(self): 173 | era_stakers = self.kusama_substrate.query_map( 174 | module='Staking', 175 | storage_function='ErasStakers', 176 | params=[2185], 177 | max_results=4, 178 | block_hash="0x61dd66907df3187fd1438463f2c87f0d596797936e0a292f6f98d12841da2325" 179 | ) 180 | 181 | records = list(era_stakers) 182 | 183 | self.assertEqual(len(records), 4) 184 | self.assertEqual(records[0][0].ss58_address, 'JCghFN7mD4ETKzMbvSVmMMPwWutJGk6Bm1yKWk8Z9KhPGeZ') 185 | self.assertEqual(records[1][0].ss58_address, 'CmNv7yFV13CMM6r9dJYgdi4UTJK7tzFEF17gmK9c3mTc2PG') 186 | self.assertEqual(records[2][0].ss58_address, 'DfishveZoxSRNRb8FtyS7ignbw6cr32eCY2w6ctLDRM1NQz') 187 | self.assertEqual(records[3][0].ss58_address, 'HmsTAS1bCtZc9FSq9nqJzZCEkhhSygtXj9TDxNgEWTHnpyQ') 188 | 189 | def test_double_map_page_size(self): 190 | era_stakers = self.kusama_substrate.query_map( 191 | module='Staking', 192 | storage_function='ErasStakers', 193 | params=[2185], 194 | max_results=4, 195 | page_size=1, 196 | block_hash="0x61dd66907df3187fd1438463f2c87f0d596797936e0a292f6f98d12841da2325" 197 | ) 198 | 199 | records = list(era_stakers) 200 | 201 | self.assertEqual(len(records), 4) 202 | self.assertEqual(records[0][0].ss58_address, 'JCghFN7mD4ETKzMbvSVmMMPwWutJGk6Bm1yKWk8Z9KhPGeZ') 203 | self.assertEqual(records[1][0].ss58_address, 'CmNv7yFV13CMM6r9dJYgdi4UTJK7tzFEF17gmK9c3mTc2PG') 204 | self.assertEqual(records[2][0].ss58_address, 'DfishveZoxSRNRb8FtyS7ignbw6cr32eCY2w6ctLDRM1NQz') 205 | self.assertEqual(records[3][0].ss58_address, 'HmsTAS1bCtZc9FSq9nqJzZCEkhhSygtXj9TDxNgEWTHnpyQ') 206 | 207 | def test_double_map_no_result(self): 208 | era_stakers = self.kusama_substrate.query_map( 209 | module='Staking', 210 | storage_function='ErasStakers', 211 | params=[21000000], 212 | block_hash="0x61dd66907df3187fd1438463f2c87f0d596797936e0a292f6f98d12841da2325" 213 | ) 214 | self.assertEqual(era_stakers.records, []) 215 | 216 | def test_nested_keys(self): 217 | 218 | result = self.kusama_substrate.query_map( 219 | module='ConvictionVoting', 220 | storage_function='VotingFor', 221 | max_results=10 222 | ) 223 | self.assertTrue(self.kusama_substrate.is_valid_ss58_address(result[0][0][0].value)) 224 | self.assertGreaterEqual(result[0][0][1], 0) 225 | 226 | def test_double_map_too_many_params(self): 227 | with self.assertRaises(ValueError) as cm: 228 | self.kusama_substrate.query_map( 229 | module='Staking', 230 | storage_function='ErasStakers', 231 | params=[21000000, 2] 232 | ) 233 | self.assertEqual('Storage function map can accept max 1 parameters, 2 given', str(cm.exception)) 234 | 235 | def test_map_with_param(self): 236 | with self.assertRaises(ValueError) as cm: 237 | self.kusama_substrate.query_map( 238 | module='System', 239 | storage_function='Account', 240 | params=[2] 241 | ) 242 | self.assertEqual('Storage function map can accept max 0 parameters, 1 given', str(cm.exception)) 243 | 244 | 245 | if __name__ == '__main__': 246 | unittest.main() 247 | -------------------------------------------------------------------------------- /test/test_runtime_call.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2022 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | from unittest.mock import MagicMock 19 | 20 | from substrateinterface import SubstrateInterface, Keypair 21 | from substrateinterface.exceptions import StorageFunctionNotFound 22 | from test import settings 23 | 24 | 25 | class RuntimeCallTestCase(unittest.TestCase): 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | cls.substrate = SubstrateInterface( 30 | url=settings.POLKADOT_NODE_URL, 31 | ss58_format=0, 32 | type_registry_preset='polkadot' 33 | ) 34 | # Create new keypair 35 | mnemonic = Keypair.generate_mnemonic() 36 | cls.keypair = Keypair.create_from_mnemonic(mnemonic) 37 | 38 | def test_core_version(self): 39 | result = self.substrate.runtime_call("Core", "version") 40 | 41 | self.assertGreater(result.value['spec_version'], 0) 42 | self.assertEqual('polkadot', result.value['spec_name']) 43 | 44 | def test_core_version_at_not_best_block(self): 45 | parent_hash = self.substrate.get_block_header()['header']['parentHash'] 46 | result = self.substrate.runtime_call("Core", "version", block_hash = parent_hash) 47 | 48 | self.assertGreater(result.value['spec_version'], 0) 49 | self.assertEqual('polkadot', result.value['spec_name']) 50 | 51 | def test_transaction_payment(self): 52 | call = self.substrate.compose_call( 53 | call_module='Balances', 54 | call_function='transfer_keep_alive', 55 | call_params={ 56 | 'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk', 57 | 'value': 3 * 10 ** 3 58 | } 59 | ) 60 | 61 | extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=self.keypair, tip=1) 62 | extrinsic_len = self.substrate.create_scale_object('u32') 63 | extrinsic_len.encode(len(extrinsic.data)) 64 | 65 | result = self.substrate.runtime_call("TransactionPaymentApi", "query_fee_details", [extrinsic, extrinsic_len]) 66 | 67 | self.assertGreater(result.value['inclusion_fee']['base_fee'], 0) 68 | self.assertEqual(0, result.value['tip']) 69 | 70 | def test_metadata_call_info(self): 71 | 72 | runtime_call = self.substrate.get_metadata_runtime_call_function("TransactionPaymentApi", "query_fee_details") 73 | param_info = runtime_call.get_param_info() 74 | self.assertEqual('Extrinsic', param_info[0]) 75 | self.assertEqual('u32', param_info[1]) 76 | 77 | runtime_call = self.substrate.get_metadata_runtime_call_function("Core", "initialise_block") 78 | param_info = runtime_call.get_param_info() 79 | self.assertEqual('u32', param_info[0]['number']) 80 | self.assertEqual('h256', param_info[0]['parent_hash']) 81 | 82 | def test_check_all_runtime_call_types(self): 83 | runtime_calls = self.substrate.get_metadata_runtime_call_functions() 84 | for runtime_call in runtime_calls: 85 | param_info = runtime_call.get_param_info() 86 | self.assertEqual(type(param_info), list) 87 | result_obj = self.substrate.create_scale_object(runtime_call.value['type']) 88 | info = result_obj.generate_type_decomposition() 89 | self.assertIsNotNone(info) 90 | 91 | def test_unknown_runtime_call(self): 92 | with self.assertRaises(ValueError): 93 | self.substrate.runtime_call("Foo", "bar") 94 | 95 | 96 | if __name__ == '__main__': 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /test/test_ss58.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2021 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | from substrateinterface.utils.ss58 import ss58_decode, ss58_encode, ss58_encode_account_index, \ 20 | ss58_decode_account_index, is_valid_ss58_address 21 | 22 | from substrateinterface import Keypair 23 | 24 | 25 | class SS58TestCase(unittest.TestCase): 26 | 27 | @classmethod 28 | def setUpClass(cls) -> None: 29 | 30 | cls.alice_keypair = Keypair.create_from_uri('//Alice') 31 | 32 | cls.subkey_pairs = [ 33 | { 34 | 'address': '5EU9mjvZdLRGyDFiBHjxrxvQuaaBpeTZCguhxM3yMX8cpZ2u', 35 | 'public_key': '0x6a5a5957ce778c174c02c151e7c4917ac127b33ad8485f579f830fc15d31bc5a', 36 | 'ss58_format': 42 37 | }, 38 | { 39 | # ecdsa 40 | 'address': '4pbsSkWcBaYoFHrKJZp5fDVUKbqSYD9dhZZGvpp3vQ5ysVs5ybV', 41 | 'public_key': '0x035676109c54b9a16d271abeb4954316a40a32bcce023ac14c8e26e958aa68fba9', 42 | 'ss58_format': 200 43 | }, 44 | { 45 | 'address': 'yGF4JP7q5AK46d1FPCEm9sYQ4KooSjHMpyVAjLnsCSWVafPnf', 46 | 'public_key': '0x66cd6cf085627d6c85af1aaf2bd10cf843033e929b4e3b1c2ba8e4aa46fe111b', 47 | 'ss58_format': 255 48 | }, 49 | { 50 | 'address': 'yGDYxQatQwuxqT39Zs4LtcTnpzE12vXb7ZJ6xpdiHv6gTu1hF', 51 | 'public_key': '0x242fd5a078ac6b7c3c2531e9bcf1314343782aeb58e7bc6880794589e701db55', 52 | 'ss58_format': 255 53 | }, 54 | { 55 | 'address': 'mHm8k9Emsvyfp3piCauSH684iA6NakctF8dySQcX94GDdrJrE', 56 | 'public_key': '0x44d5a3ac156335ea99d33a83c57c7146c40c8e2260a8a4adf4e7a86256454651', 57 | 'ss58_format': 4242 58 | }, 59 | { 60 | 'address': 'r6Gr4gaMP8TsjhFbqvZhv3YvnasugLiRJpzpRHifsqqG18UXa', 61 | 'public_key': '0x88f01441682a17b52d6ae12d1a5670cf675fd254897efabaa5069eb3a701ab73', 62 | 'ss58_format': 14269 63 | } 64 | ] 65 | 66 | def test_encode_key_pair_alice_address(self): 67 | self.assertEqual(self.alice_keypair.ss58_address, "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") 68 | 69 | def test_encode_1_byte_account_index(self): 70 | self.assertEqual('F7NZ', ss58_encode_account_index(1)) 71 | 72 | def test_encode_1_byte_account_index_with_format(self): 73 | self.assertEqual('g4b', ss58_encode_account_index(1, ss58_format=2)) 74 | self.assertEqual('g4b', ss58_encode('0x01', ss58_format=2)) 75 | 76 | def test_encode_2_bytes_account_index(self): 77 | self.assertEqual('3xygo', ss58_encode_account_index(256, ss58_format=2)) 78 | self.assertEqual('3xygo', ss58_encode('0x0001', ss58_format=2)) 79 | 80 | def test_encode_4_bytes_account_index(self): 81 | self.assertEqual('zswfoZa', ss58_encode_account_index(67305985, ss58_format=2)) 82 | self.assertEqual('zswfoZa', ss58_encode('0x01020304', ss58_format=2)) 83 | 84 | def test_encode_8_bytes_account_index(self): 85 | self.assertEqual('848Gh2GcGaZia', ss58_encode('0x2a2c0a0000000000', ss58_format=2)) 86 | 87 | def test_decode_1_byte_account_index(self): 88 | self.assertEqual(1, ss58_decode_account_index('F7NZ')) 89 | 90 | def test_decode_2_bytes_account_index(self): 91 | self.assertEqual(256, ss58_decode_account_index('3xygo')) 92 | 93 | def test_decode_4_bytes_account_index(self): 94 | self.assertEqual(67305985, ss58_decode_account_index('zswfoZa')) 95 | 96 | def test_decode_8_bytes_account_index(self): 97 | self.assertEqual(666666, ss58_decode_account_index('848Gh2GcGaZia')) 98 | 99 | def test_encode_33_byte_address(self): 100 | self.assertEqual( 101 | 'KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou', 102 | ss58_encode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') 103 | ) 104 | 105 | def test_encode_with_2_byte_prefix(self): 106 | public_key = ss58_decode('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua') 107 | 108 | self.assertEqual( 109 | 'yGHU8YKprxHbHdEv7oUK4rzMZXtsdhcXVG2CAMyC9WhzhjH2k', 110 | ss58_encode(public_key, ss58_format=255) 111 | ) 112 | 113 | def test_encode_subkey_generated_pairs(self): 114 | for subkey_pair in self.subkey_pairs: 115 | self.assertEqual( 116 | subkey_pair['address'], 117 | ss58_encode(address=subkey_pair['public_key'], ss58_format=subkey_pair['ss58_format']) 118 | ) 119 | 120 | def test_decode_subkey_generated_pairs(self): 121 | for subkey_pair in self.subkey_pairs: 122 | self.assertEqual( 123 | subkey_pair['public_key'], 124 | '0x' + ss58_decode(address=subkey_pair['address'], valid_ss58_format=subkey_pair['ss58_format']) 125 | ) 126 | 127 | def test_invalid_ss58_format_range_exceptions(self): 128 | with self.assertRaises(ValueError) as cm: 129 | ss58_encode(self.alice_keypair.public_key, ss58_format=-1) 130 | 131 | self.assertEqual('Invalid value for ss58_format', str(cm.exception)) 132 | 133 | with self.assertRaises(ValueError) as cm: 134 | ss58_encode(self.alice_keypair.public_key, ss58_format=16384) 135 | 136 | self.assertEqual('Invalid value for ss58_format', str(cm.exception)) 137 | 138 | def test_invalid_reserved_ss58_format(self): 139 | with self.assertRaises(ValueError) as cm: 140 | ss58_encode(self.alice_keypair.public_key, ss58_format=46) 141 | 142 | self.assertEqual('Invalid value for ss58_format', str(cm.exception)) 143 | 144 | with self.assertRaises(ValueError) as cm: 145 | ss58_encode(self.alice_keypair.public_key, ss58_format=47) 146 | 147 | self.assertEqual('Invalid value for ss58_format', str(cm.exception)) 148 | 149 | def test_invalid_public_key(self): 150 | with self.assertRaises(ValueError) as cm: 151 | ss58_encode(self.alice_keypair.public_key[:30]) 152 | 153 | self.assertEqual('Invalid length for address', str(cm.exception)) 154 | 155 | def test_decode_public_key(self): 156 | self.assertEqual( 157 | '0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077', 158 | ss58_decode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') 159 | ) 160 | 161 | def test_decode_reserved_ss58_formats(self): 162 | with self.assertRaises(ValueError) as cm: 163 | ss58_decode('MGP3U1wqNhFofseKXU7B6FcZuLbvQvJFyin1EvQM65mBcNsY8') 164 | 165 | self.assertEqual('46 is a reserved SS58 format', str(cm.exception)) 166 | 167 | with self.assertRaises(ValueError) as cm: 168 | ss58_decode('MhvaLBvSb5jhjrftHLQPAvJegnpXgyDTE1ZprRNzAcfQSRdbL') 169 | 170 | self.assertEqual('47 is a reserved SS58 format', str(cm.exception)) 171 | 172 | def test_invalid_ss58_format_check(self): 173 | with self.assertRaises(ValueError) as cm: 174 | ss58_decode('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua', valid_ss58_format=2) 175 | 176 | self.assertEqual('Invalid SS58 format', str(cm.exception)) 177 | 178 | def test_decode_invalid_checksum(self): 179 | with self.assertRaises(ValueError) as cm: 180 | ss58_decode('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQub') 181 | 182 | self.assertEqual('Invalid checksum', str(cm.exception)) 183 | 184 | def test_decode_invalid_length(self): 185 | with self.assertRaises(ValueError) as cm: 186 | ss58_decode('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQubsdhfjksdhfkj') 187 | 188 | self.assertEqual('Invalid address length', str(cm.exception)) 189 | 190 | def test_decode_empty_string(self): 191 | with self.assertRaises(ValueError) as cm: 192 | ss58_decode('') 193 | 194 | self.assertEqual('Empty address provided', str(cm.exception)) 195 | 196 | def test_is_valid_ss58_address(self): 197 | self.assertTrue(is_valid_ss58_address('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY')) 198 | self.assertTrue(is_valid_ss58_address('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', valid_ss58_format=42)) 199 | self.assertFalse(is_valid_ss58_address('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', valid_ss58_format=2)) 200 | 201 | self.assertTrue(is_valid_ss58_address('GLdQ4D4wkeEJUX8DBT9HkpycFVYQZ3fmJyQ5ZgBRxZ4LD3S', valid_ss58_format=2)) 202 | self.assertFalse(is_valid_ss58_address('GLdQ4D4wkeEJUX8DBT9HkpycFVYQZ3fmJyQ5ZgBRxZ4LD3S', valid_ss58_format=42)) 203 | self.assertFalse(is_valid_ss58_address('GLdQ4D4wkeEJUX8DBT9HkpycFVYQZ3fmJyQ5ZgBRxZ4LD3S', valid_ss58_format=0)) 204 | self.assertTrue(is_valid_ss58_address('12gX42C4Fj1wgtfgoP624zeHrcPBqzhb4yAENyvFdGX6EUnN', valid_ss58_format=0)) 205 | 206 | self.assertFalse(is_valid_ss58_address('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQ')) 207 | self.assertFalse(is_valid_ss58_address('6GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY')) 208 | self.assertFalse(is_valid_ss58_address('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d')) 209 | self.assertFalse(is_valid_ss58_address('d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d')) 210 | self.assertFalse(is_valid_ss58_address('incorrect_string')) 211 | 212 | 213 | if __name__ == '__main__': 214 | unittest.main() 215 | -------------------------------------------------------------------------------- /test/test_subscriptions.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2021 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | from substrateinterface import SubstrateInterface 20 | from test import settings 21 | 22 | 23 | class SubscriptionsTestCase(unittest.TestCase): 24 | 25 | @classmethod 26 | def setUpClass(cls): 27 | cls.substrate = SubstrateInterface( 28 | url=settings.POLKADOT_NODE_URL 29 | ) 30 | 31 | def test_query_subscription(self): 32 | 33 | def subscription_handler(obj, update_nr, subscription_id): 34 | 35 | return {'update_nr': update_nr, 'subscription_id': subscription_id} 36 | 37 | result = self.substrate.query("System", "Events", [], subscription_handler=subscription_handler) 38 | 39 | self.assertIsNotNone(result['subscription_id']) 40 | 41 | def test_subscribe_storage_multi(self): 42 | 43 | def subscription_handler(storage_key, updated_obj, update_nr, subscription_id): 44 | return {'update_nr': update_nr, 'subscription_id': subscription_id} 45 | 46 | storage_keys = [ 47 | self.substrate.create_storage_key( 48 | "System", "Account", ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"] 49 | ), 50 | self.substrate.create_storage_key( 51 | "System", "Account", ["5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"] 52 | ) 53 | ] 54 | 55 | result = self.substrate.subscribe_storage( 56 | storage_keys=storage_keys, subscription_handler=subscription_handler 57 | ) 58 | 59 | self.assertIsNotNone(result['subscription_id']) 60 | 61 | def test_subscribe_new_heads(self): 62 | 63 | def block_subscription_handler(obj, update_nr, subscription_id): 64 | return obj['header']['number'] 65 | 66 | result = self.substrate.subscribe_block_headers(block_subscription_handler, finalized_only=True) 67 | 68 | self.assertGreater(result, 0) 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /test/test_type_registry.py: -------------------------------------------------------------------------------- 1 | # Python Substrate Interface Library 2 | # 3 | # Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import os 17 | import unittest 18 | 19 | from scalecodec.base import ScaleBytes, RuntimeConfigurationObject 20 | from scalecodec.type_registry import load_type_registry_file, load_type_registry_preset 21 | 22 | from substrateinterface import SubstrateInterface, Keypair, KeypairType 23 | from test import settings 24 | 25 | 26 | class KusamaTypeRegistryTestCase(unittest.TestCase): 27 | 28 | @classmethod 29 | def setUpClass(cls): 30 | cls.substrate = SubstrateInterface( 31 | url=settings.KUSAMA_NODE_URL, 32 | ss58_format=2, 33 | type_registry_preset='kusama' 34 | ) 35 | 36 | def test_type_registry_compatibility(self): 37 | 38 | for scale_type in self.substrate.get_type_registry(): 39 | obj = self.substrate.runtime_config.get_decoder_class(scale_type) 40 | 41 | self.assertIsNotNone(obj, '{} not supported'.format(scale_type)) 42 | 43 | 44 | class PolkadotTypeRegistryTestCase(unittest.TestCase): 45 | 46 | @classmethod 47 | def setUpClass(cls): 48 | cls.substrate = SubstrateInterface( 49 | url=settings.POLKADOT_NODE_URL, 50 | ss58_format=0, 51 | type_registry_preset='polkadot' 52 | ) 53 | 54 | def test_type_registry_compatibility(self): 55 | 56 | for scale_type in self.substrate.get_type_registry(): 57 | 58 | obj = self.substrate.runtime_config.get_decoder_class(scale_type) 59 | 60 | self.assertIsNotNone(obj, '{} not supported'.format(scale_type)) 61 | 62 | 63 | class RococoTypeRegistryTestCase(unittest.TestCase): 64 | 65 | @classmethod 66 | def setUpClass(cls): 67 | cls.substrate = SubstrateInterface( 68 | url=settings.ROCOCO_NODE_URL, 69 | ss58_format=42, 70 | type_registry_preset='rococo' 71 | ) 72 | 73 | def test_type_registry_compatibility(self): 74 | 75 | for scale_type in self.substrate.get_type_registry(): 76 | 77 | obj = self.substrate.runtime_config.get_decoder_class(scale_type) 78 | 79 | self.assertIsNotNone(obj, '{} not supported'.format(scale_type)) 80 | 81 | # 82 | # class DevelopmentTypeRegistryTestCase(unittest.TestCase): 83 | # 84 | # @classmethod 85 | # def setUpClass(cls): 86 | # cls.substrate = SubstrateInterface( 87 | # url="ws://127.0.0.1:9944", 88 | # ss58_format=42, 89 | # type_registry_preset='development' 90 | # ) 91 | # 92 | # def test_type_registry_compatibility(self): 93 | # 94 | # for scale_type in self.substrate.get_type_registry(): 95 | # 96 | # obj = self.substrate.runtime_config.get_decoder_class(scale_type) 97 | # 98 | # self.assertIsNotNone(obj, '{} not supported'.format(scale_type)) 99 | 100 | 101 | class ReloadTypeRegistryTestCase(unittest.TestCase): 102 | 103 | def setUp(self) -> None: 104 | self.substrate = SubstrateInterface( 105 | url='dummy', 106 | ss58_format=42, 107 | type_registry_preset='test' 108 | ) 109 | 110 | def test_initial_correct_type_local(self): 111 | decoding_class = self.substrate.runtime_config.type_registry['types']['index'] 112 | self.assertEqual(self.substrate.runtime_config.get_decoder_class('u32'), decoding_class) 113 | 114 | def test_reloading_use_remote_preset(self): 115 | 116 | # Intentionally overwrite type in local preset 117 | u32_cls = self.substrate.runtime_config.get_decoder_class('u32') 118 | u64_cls = self.substrate.runtime_config.get_decoder_class('u64') 119 | 120 | self.substrate.runtime_config.type_registry['types']['index'] = u64_cls 121 | 122 | self.assertEqual(u64_cls, self.substrate.runtime_config.get_decoder_class('Index')) 123 | 124 | # Reload type registry 125 | self.substrate.reload_type_registry() 126 | 127 | self.assertEqual(u32_cls, self.substrate.runtime_config.get_decoder_class('Index')) 128 | 129 | def test_reloading_use_local_preset(self): 130 | 131 | # Intentionally overwrite type in local preset 132 | u32_cls = self.substrate.runtime_config.get_decoder_class('u32') 133 | u64_cls = self.substrate.runtime_config.get_decoder_class('u64') 134 | 135 | self.substrate.runtime_config.type_registry['types']['index'] = u64_cls 136 | 137 | self.assertEqual(u64_cls, self.substrate.runtime_config.get_decoder_class('Index')) 138 | 139 | # Reload type registry 140 | self.substrate.reload_type_registry(use_remote_preset=False) 141 | 142 | self.assertEqual(u32_cls, self.substrate.runtime_config.get_decoder_class('Index')) 143 | 144 | 145 | class AutodiscoverV14RuntimeTestCase(unittest.TestCase): 146 | runtime_config = None 147 | metadata_obj = None 148 | metadata_fixture_dict = None 149 | 150 | @classmethod 151 | def setUpClass(cls): 152 | module_path = os.path.dirname(__file__) 153 | cls.metadata_fixture_dict = load_type_registry_file( 154 | os.path.join(module_path, 'fixtures', 'metadata_hex.json') 155 | ) 156 | cls.runtime_config = RuntimeConfigurationObject(implements_scale_info=True) 157 | cls.runtime_config.update_type_registry(load_type_registry_preset("core")) 158 | 159 | cls.metadata_obj = cls.runtime_config.create_scale_object( 160 | 'MetadataVersioned', data=ScaleBytes(cls.metadata_fixture_dict['V14']) 161 | ) 162 | cls.metadata_obj.decode() 163 | 164 | def setUp(self) -> None: 165 | 166 | class MockedSubstrateInterface(SubstrateInterface): 167 | 168 | def rpc_request(self, method, params, result_handler=None): 169 | 170 | if method == 'system_chain': 171 | return { 172 | 'jsonrpc': '2.0', 173 | 'result': 'test', 174 | 'id': self.request_id 175 | } 176 | 177 | return super().rpc_request(method, params, result_handler) 178 | 179 | self.substrate = MockedSubstrateInterface( 180 | url=settings.KUSAMA_NODE_URL 181 | ) 182 | 183 | def test_type_reg_preset_applied(self): 184 | self.substrate.init_runtime() 185 | self.assertIsNotNone(self.substrate.runtime_config.get_decoder_class('SpecificTestType')) 186 | 187 | 188 | class AutodetectAddressTypeTestCase(unittest.TestCase): 189 | 190 | def test_default_substrate_address(self): 191 | substrate = SubstrateInterface( 192 | url=settings.POLKADOT_NODE_URL, auto_discover=False 193 | ) 194 | 195 | keypair_alice = Keypair.create_from_uri('//Alice', ss58_format=substrate.ss58_format) 196 | 197 | call = substrate.compose_call( 198 | call_module='Balances', 199 | call_function='transfer_keep_alive', 200 | call_params={ 201 | 'dest': keypair_alice.ss58_address, 202 | 'value': 2000 203 | } 204 | ) 205 | 206 | extrinsic = substrate.create_signed_extrinsic(call, keypair_alice) 207 | 208 | self.assertEqual(extrinsic.value['address'], f'0x{keypair_alice.public_key.hex()}') 209 | 210 | def test_eth_address(self): 211 | substrate = SubstrateInterface( 212 | url=settings.MOONBEAM_NODE_URL, auto_discover=False 213 | ) 214 | 215 | keypair_alice = Keypair.create_from_mnemonic(Keypair.generate_mnemonic(), crypto_type=KeypairType.ECDSA) 216 | 217 | call = substrate.compose_call( 218 | call_module='Balances', 219 | call_function='transfer_keep_alive', 220 | call_params={ 221 | 'dest': keypair_alice.ss58_address, 222 | 'value': 2000 223 | } 224 | ) 225 | 226 | extrinsic = substrate.create_signed_extrinsic(call, keypair_alice) 227 | 228 | self.assertEqual(extrinsic.value['address'], f'0x{keypair_alice.public_key.hex()}') 229 | 230 | 231 | if __name__ == '__main__': 232 | unittest.main() 233 | --------------------------------------------------------------------------------