Package: polymath-substrate-interface
This project is forked fromhttps://github.com/polkascan/py-substrate-interfacePython Polymath Substrate InterfacePython Polymath Substrate Interface LibraryDescriptionThis library specializes in interfacing with a Polymesh node, providing additional convenience methods to deal with
SCALE encoding/decoding (the default output and input format of the Substrate JSONRPC), metadata parsing, type registry
management and versioning of types.Table of ContentsDocumentationInstallationInitializationAutodiscover modeManually set required propertiesSubstrate Node TemplateFeaturesGet extrinsics for a certain blockSubscribe to new block headersStorage queriesStorage subscriptionsQuery a mapped storage functionCreate and send signed extrinsicsExamining the ExtrinsicReceipt objectink! contract interfacingCreate mortal extrinsicsKeypair creation and signingCreating keypairs with soft and hard key derivation pathsGetting estimate of network fees for extrinsic in advanceOffline signing of extrinsicsAccessing runtime constantsKeeping type registry presets up to dateCleanup and context managerLicenseDocumentationhttps://polkascan.github.io/py-substrate-interface/Installationpipinstallpolymath-substrate-interfaceInitializationThe following examples show how to initialize for Polymesh chain:Westendsubstrate=SubstrateInterface(url="wss://westend-rpc.polkadot.io",ss58_format=42,type_registry_preset='westend')Substrate Node TemplateCompatible withhttps://github.com/substrate-developer-hub/substrate-node-templatesubstrate=SubstrateInterface(url="ws://127.0.0.1:9944",ss58_format=42,type_registry_preset='substrate-node-template')If custom types are introduced in the Substrate chain, the following example will add compatibility by creating a custom type
registry JSON file and including this during initialization:{"runtime_id":2,"types":{"MyCustomInt":"u32","MyStruct":{"type":"struct","type_mapping":[["account","AccountId"],["message","Vec<u8>"]]}},"versioning":[]}custom_type_registry=load_type_registry_file("my-custom-types.json")substrate=SubstrateInterface(url="ws://127.0.0.1:9944",ss58_format=42,type_registry_preset='substrate-node-template',type_registry=custom_type_registry)Keeping type registry presets up to dateWhen on-chain runtime upgrades occur, types used in call- or storage functions can be added or modified. Therefor it is
important to keep the type registry presets up to date. At the moment the type registry for Polymesh is being actively maintained for this library and an check and update procedure can be triggered with:substrate.update_type_registry_presets()ExamplesGet extrinsics for a certain block# Set block_hash to None for chaintipblock_hash="0x51d15792ff3c5ee9c6b24ddccd95b377d5cccc759b8e76e5de9250cf58225087"# Retrieve extrinsics in blockresult=substrate.get_block(block_hash=block_hash)forextrinsicinresult['extrinsics']:ifextrinsic.address:signed_by_address=extrinsic.address.valueelse:signed_by_address=Noneprint('\nPallet:{}\nCall:{}\nSigned by:{}'.format(extrinsic.call_module.name,extrinsic.call.name,signed_by_address))# Loop through call paramsforparaminextrinsic.params:ifparam['type']=='Compact<Balance>':param['value']='{}{}'.format(param['value']/10**substrate.token_decimals,substrate.token_symbol)print("Param '{}':{}".format(param['name'],param['value']))Subscribe to new block headersdefsubscription_handler(obj,update_nr,subscription_id):print(f"New block #{obj['header']['number']}produced by{obj['author']}")ifupdate_nr>10:return{'message':'Subscription will cancel when a value is returned','updates_processed':update_nr}result=substrate.subscribe_block_headers(subscription_handler,include_author=True)Storage queriesThe modules and storage functions are provided in the metadata (seesubstrate.get_metadata_storage_functions()),
parameters will be automatically converted to SCALE-bytes (also including decoding of SS58 addresses).Example:result=substrate.query(module='System',storage_function='Account',params=['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T'])print(result.value['nonce'])#  7695print(result.value['data']['free'])# 635278638077956496Or get the account info at a specific block hash:account_info=substrate.query(module='System',storage_function='Account',params=['F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T'],block_hash='0x176e064454388fd78941a0bace38db424e71db9d5d5ed0272ead7003a02234fa')print(account_info.value['nonce'])#  7673print(account_info.value['data']['free'])# 637747267365404068Storage subscriptionsWhen a callable is passed as kwargsubscription_handler, there will be a subscription created for given storage query.
Updates will be pushed to the callable and will block execution until a final value is returned. This value will be returned
as a result of the query and finally automatically unsubscribed from further updates.defsubscription_handler(account_info_obj,update_nr,subscription_id):ifupdate_nr==0:print('Initial account data:',account_info_obj.value)ifupdate_nr>0:# Do something with the updateprint('Account data changed:',account_info_obj.value)# The execution will block until an arbitrary value is returned, which will be the result of the `query`ifupdate_nr>5:returnaccount_info_objresult=substrate.query("System","Account",["5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY"],subscription_handler=subscription_handler)print(result)Query a mapped storage functionMapped storage functions can be iterated over all key/value pairs, for these type of storage functionsquery_mapcan be used.The result is aQueryMapResultobject, which is an iterator:# Retrieve the first 199 System.Account entriesresult=substrate.query_map('System','Account',max_results=199)foraccount,account_infoinresult:print(f"Free balance of account '{account.value}':{account_info.value['data']['free']}")These results are transparantly retrieved in batches capped by thepage_sizekwarg, currently the
maximumpage_sizerestricted by the RPC node is 1000# Retrieve all System.Account entries in batches of 200 (automatically appended by `QueryMapResult` iterator)result=substrate.query_map('System','Account',page_size=200,max_results=400)foraccount,account_infoinresult:print(f"Free balance of account '{account.value}':{account_info.value['data']['free']}")Querying aDoubleMapstorage function:era_stakers=substrate.query_map(module='Staking',storage_function='ErasStakers',params=[2100])Create and send signed extrinsicsThe following code snippet illustrates how to create a call, wrap it in a signed extrinsic and send it to the network:fromsubstrateinterfaceimportSubstrateInterface,Keypairfromsubstrateinterface.exceptionsimportSubstrateRequestExceptionsubstrate=SubstrateInterface(url="ws://127.0.0.1:9944",ss58_format=42,type_registry_preset='kusama')keypair=Keypair.create_from_mnemonic('episode together nose spoon dose oil faculty zoo ankle evoke admit walnut')call=substrate.compose_call(call_module='Balances',call_function='transfer',call_params={'dest':'5E9oDs9PjpsBbxXxRE9uMaZZhnBAV38n2ouLB28oecBDdeQo','value':1*10**12})extrinsic=substrate.create_signed_extrinsic(call=call,keypair=keypair)try:receipt=substrate.submit_extrinsic(extrinsic,wait_for_inclusion=True)print("Extrinsic '{}' sent and included in block '{}'".format(receipt.extrinsic_hash,receipt.block_hash))exceptSubstrateRequestExceptionase:print("Failed to send:{}".format(e))Thewait_for_inclusionkeyword argument used in the example above will block giving the result until it gets
confirmation from the node that the extrinsic is succesfully included in a block. Thewait_for_finalizationkeyword
will wait until extrinsic is finalized. Note this feature is only available for websocket connections.Examining the ExtrinsicReceipt objectThesubstrate.submit_extrinsicexample above returns anExtrinsicReceiptobject, which contains information about the on-chain
execution of the extrinsic. Because theblock_hashis necessary to retrieve the triggered events from storage, most
information is only available whenwait_for_inclusion=Trueorwait_for_finalization=Trueis used when submitting
an extrinsic.Examples:receipt=substrate.submit_extrinsic(extrinsic,wait_for_inclusion=True)print(receipt.is_success)# Falseprint(receipt.weight)# 216625000print(receipt.total_fee_amount)# 2749998966print(receipt.error_message['name'])# 'LiquidityRestrictions'ExtrinsicReceiptobjects can also be created for all existing extrinsics on-chain:receipt=ExtrinsicReceipt(substrate=substrate,extrinsic_hash="0x56fea3010910bd8c0c97253ffe308dc13d1613b7e952e7e2028257d2b83c027a",block_hash="0x04fb003f8bc999eeb284aa8e74f2c6f63cf5bd5c00d0d0da4cd4d253a643e4c9")print(receipt.is_success)# Falseprint(receipt.extrinsic.call_module.name)# 'Identity'print(receipt.extrinsic.call.name)# 'remove_sub'print(receipt.weight)# 359262000print(receipt.total_fee_amount)# 2483332406print(receipt.error_message['docs'])# [' Sender is not a sub-account.']foreventinreceipt.triggered_events:print(f'*{event.value}')ink! contract interfacingDeploy a contractTested oncanvas-nodewith theFlipper contract from the tutorial_:substrate=SubstrateInterface(url="ws://127.0.0.1:9944",type_registry_preset='canvas')keypair=Keypair.create_from_uri('//Alice')# Deploy contractcode=ContractCode.create_from_contract_files(metadata_file=os.path.join(os.path.dirname(__file__),'assets','flipper.json'),wasm_file=os.path.join(os.path.dirname(__file__),'assets','flipper.wasm'),substrate=substrate)contract=code.deploy(keypair=keypair,endowment=10**15,gas_limit=1000000000000,constructor="new",args={'init_value':True},upload_code=True)print(f'✅ Deployed @{contract.contract_address}')Work with an existing instance:# Create contract instance from deterministic addresscontract=ContractInstance.create_from_address(contract_address=contract_address,metadata_file=os.path.join(os.path.dirname(__file__),'assets','flipper.json'),substrate=substrate)Read data from a contract:result=contract.read(keypair,'get')print('Current value of "get":',result.contract_result_data)Execute a contract call# Do a gas estimation of the messagegas_predit_result=contract.read(keypair,'flip')print('Result of dry-run: ',gas_predit_result.contract_result_data)print('Gas estimate: ',gas_predit_result.gas_consumed)# Do the actual transferprint('Executing contract call...')contract_receipt=contract.exec(keypair,'flip',args={},gas_limit=gas_predit_result.gas_consumed)print(f'Events triggered in contract:{contract_receipt.contract_events}')See completecode examplefor more detailsCreate mortal extrinsicsBy default,immortalextrinsics are created, which means they have an indefinite lifetime for being included in a
block. However, it is recommended to use specify an expiry window, so you know after a certain amount of time if the
extrinsic is not included in a block, it will be invalidated.extrinsic=substrate.create_signed_extrinsic(call=call,keypair=keypair,era={'period':64})Theperiodspecifies the number of blocks the extrinsic is valid counted from current head.Keypair creation and signingmnemonic=Keypair.generate_mnemonic()keypair=Keypair.create_from_mnemonic(mnemonic)signature=keypair.sign("Test123")ifkeypair.verify("Test123",signature):print('Verified')By default, a keypair is using SR25519 cryptography, alternatively ED25519 can be explictly specified:keypair=Keypair.create_from_mnemonic(mnemonic,crypto_type=KeypairType.ED25519)Creating keypairs with soft and hard key derivation pathsmnemonic=Keypair.generate_mnemonic()keypair=Keypair.create_from_uri(mnemonic+'//hard/soft')By omitting the mnemonic the default development mnemonic is used:keypair=Keypair.create_from_uri('//Alice')Getting estimate of network fees for extrinsic in advancekeypair=Keypair(ss58_address="EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk")call=substrate.compose_call(call_module='Balances',call_function='transfer',call_params={'dest':'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk','value':2*10**3})payment_info=substrate.get_payment_info(call=call,keypair=keypair)# {'class': 'normal', 'partialFee': 2499999066, 'weight': 216625000}Offline signing of extrinsicsThis example generates a signature payload which can be signed on another (offline) machine and later on sent to the
network with the generated signature.Generate signature payload on online machine:substrate=SubstrateInterface(url="ws://127.0.0.1:9944",ss58_format=42,type_registry_preset='substrate-node-template',)call=substrate.compose_call(call_module='Balances',call_function='transfer',call_params={'dest':'5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY','value':2*10**8})era={'period':64,'current':22719}nonce=0signature_payload=substrate.generate_signature_payload(call=call,era=era,nonce=nonce)Then on another (offline) machine generate the signature with givensignature_payload:keypair=Keypair.create_from_mnemonic("nature exchange gasp toy result bacon coin broccoli rule oyster believe lyrics")signature=keypair.sign(signature_payload)Finally on the online machine send the extrinsic with generated signature:keypair=Keypair(ss58_address="5EChUec3ZQhUvY1g52ZbfBVkqjUY9Kcr6mcEvQMbmd38shQL")extrinsic=substrate.create_signed_extrinsic(call=call,keypair=keypair,era=era,nonce=nonce,signature=signature)result=substrate.submit_extrinsic(extrinsic=extrinsic)print(result.extrinsic_hash)Accessing runtime constantsAll runtime constants are provided in the metadata (seesubstrate.get_metadata_constants()),
to access these as a decodedScaleTypeyou can use the functionsubstrate.get_constant():constant=substrate.get_constant("Balances","ExistentialDeposit")print(constant.value)# 10000000000Cleanup and context managerAt the end of the lifecycle of aSubstrateInterfaceinstance, calling theclose()method will do all the necessary
cleanup, like closing the websocket connection.When using the context manager this will be done automatically:withSubstrateInterface(url="wss://rpc.polkadot.io")assubstrate:events=substrate.query("System","Events")# connection is now closedKeeping type registry presets up to dateWhen on-chain runtime upgrades occur, types used in call- or storage functions can be added or modified. Therefor it is
important to keep the type registry presets up to date, otherwise this can lead to decoding errors likeRemainingScaleBytesNotEmptyException.At the moment the type registry presets for Polkadot, Kusama, Rococo and
Westend are being actively maintained for this library, and a check and update procedure can be triggered with:substrate.reload_type_registry()This will also activate the updated preset for the current instance.It is also possible to always use
the remote type registry preset from Github with theuse_remote_presetkwarg when instantiating:substrate=SubstrateInterface(url="wss://rpc.polkadot.io",ss58_format=0,type_registry_preset='polkadot',use_remote_preset=True)To check for updates after instantiating thesubstrateobject, usingsubstrate.reload_type_registry()will download
the most recent type registry preset from Github and apply changes to current object.Licensehttps://github.com/PolymathNetwork/py-substrate-interface/blob/master/LICENSE