Package: patract-interface
Himalia PatractPySubstrate Contract SDK for Python As a part of HimaliaPatractPy is a contract SDK to support the development of Python scripts that interact with contracts, including automated scripts to support testing. Unlike PatractGo, PatractPy is mainly for script development, so PatractPy mainly completes contract-related RPC interfaces, and completes contract deployment and instantiation-related operations.PatractPy will provide support foreuropaenv, which is a good environment for contract exec sandbox,
With PatractPy, we can write contract unittest by python, which is more friendly to developer and can easy use other test tools.PatractPy will be based onpolkascan's Python Substrate Interface, which is a Python sdk for Substrate.Element Group for disscusion:https://app.element.io/#/room/#PatractLabsDev:matrix.orgPatractPy will achieve the following support:Some support that missing inpolkascan's Python Substrate Interface, which is needed for contractsProvide Scanning and monitoring support for contract to do statistics and analysisProvide a SDK development example for ERC20 contractSupport For unittest to canvas oreuropaenv.UsageThe sdk is inhttps://pypi.org/project/patract-interface/0.3.1/install package:pip3install-Upatract-interfaceuse in python:frompatractinterfaceimportContractFactory,ContractAPI...Sosomething...Quick startimportosfromsubstrateinterfaceimportSubstrateInterface,Keypairfrompatractinterface.contractimportContractAPI,ContractFactoryfrompatractinterface.observerimportContractObserverdefmain():# use [europa](https://github.com/patractlabs/europa) as test node endpoint, notice `type_registry` should set correctly.substrate=SubstrateInterface(url='ws://127.0.0.1:9944',type_registry_preset="default",type_registry={'types':{'LookupSource':'MultiAddress'}})# load deployer keyalice=Keypair.create_from_uri('//Alice')bob=Keypair.create_from_uri('//Bob')# 1. load a contract from WASM file and metadata.json file (Those files is complied by [ink!](https://github.com/paritytech/ink))# in this example, we use `ink/example/erc20` contract as example.contract=ContractFactory.create_from_file(substrate=substrate,# should provide a subtrate endpointcode_file=os.path.join(os.path.dirname(__file__),'res','erc20.wasm'),metadata_file=os.path.join(os.path.dirname(__file__),'res','erc20.json'))# 2. instantiate the uploaded code as a contract instanceerc20_ins=contract.new(alice,1000000*(10**15),endowment=2*10**10,gas_limit=20000000000,deployment_salt="0x12")# 2.1 create a observer to listen eventobserver=ContractObserver(erc20_ins.contract_address,erc20_ins.metadata,substrate)# 3. send a transfer call for this contractres=erc20_ins.transfer(alice,bob.ss58_address,100000,gas_limit=20000000000)print('transfer res',res.is_success)defon_transfer(num,evt):print("on_transfer in{}:{}{}{}".format(num,evt['from'],evt['to'],evt['value']))defon_approval(num,evt):print("on_approval in{}:{}{}{}".format(num,evt['owner'],evt['spender'],evt['value']))# 4 set event callbackobserver.scanEvents(handlers={'Transfer':on_transfer,'Approve':on_approval})if__name__=="__main__":main()passTestFor Unittest, should installeuropaat first.# install v1.0.0 europa to localcargoinstalleuropa--git=https://github.com/patractlabs/europa--tag=v1.0.0--force--locked# check europa versioneuropa--versionAll of test pased by europa environment.Installpytestandexecutorto run test:pip3installpytest
pip3installexecutor
pytest./test--log-cli-levelinfoBasic Apis For ContractsAspolkascan's Python Substrate Interfacehas provide some support to contract api, so we not need to important the api for contract calls, but there is some api to add:SubstrateSubscriberis a subscriber support to subscribe data changes in chain, for example, the events in chain.get_contract_event_typeadd event decode support for contracts.The basic api split into 2 parts:Contract, include:contractExecutor: This api could construct an extrinsic to call a contract, would be packed into a block and change state.contractReader: This api could construct a rpc request to call a contract, do not pack into a block and do not change any state.contractCreator: This api is used forinstantiatea contract and holding the WASM code and metadata, receive following parameters:gas_limitendowmentdeployment_salt(saltparameter ininstantiate)ContractAPI: This api is used forcalla contract, is a wrapper forcontractExecutorandcontractReader, developers could use this api to react with contracts. This api could create a instance depends on the metadata, auto generate the contract access functions based on the contract. And the auto-gen functions receive the parameters which defined in contracts, besides receive following common parameters:gas_limitvalue(notice, if current call'spayableis false, thisvaluemust be0)ContractFactory: This api is used for constructing a contract instance in python, and developer could use this instance to access contract. This is a wrapper forcontractCreatorandContractAPIObserver, include:ContractObserver: This api is used for listen the events in contracts.All methods which belong to the instance ofContractAPIandContractFactoryreceive a keypair as the first parameter, as the sender for this operation. And from the second parameter, receive the parameters defined in contracts.ContractFactoryandContractAPIis used to react with contractswe add a factory to put code and deploy contracts to chain:factory=ContractFactory.create_from_file(substrate=substrate,code_file=os.path.join(os.path.dirname(__file__),'contract','erc20.wasm'),metadata_file=os.path.join(os.path.dirname(__file__),'contract','erc20.json'))# this api is `ContractAPI`api=factory.new(alice,1000000*(10**15),endowment=10**15,gas_limit=1000000000000)print(api.contract_address)# contract_address is the deployed contractThe factory will generate constructors from metadata file.We add api by metadata for Contract, api will auto generate caller for contract from metadata:# create a ContractAPI from an existed contract addressapi=ContractAPI(contract_address,contract_metadata,substrate)# api will auto generate caller for contract from metadataalice_balance_old=api.balance_of(bob,alice.ss58_address)# bob is the keypair for `//Bob`res=api.transfer(alice,bob.ss58_address,100000,gas_limit=20000000000)logging.info(f'transfer res{res.error_message}')print(res.is_success)alice_balance=api.balance_of(bob,alice.ss58_address)logging.info(f'transfer alice_balance{alice_balance}')bob_balance=api.balance_of(bob,bob.ss58_address)logging.info(f'transfer bob_balance{bob_balance}')The api will generate exec and read api from metadata file, for example:{"args":[{"name":"owner","type":{"displayName":["AccountId"],"type":5}}],"docs":[" Returns the account balance for the specified `owner`.",""," Returns `0` if the account is non-existent."],"mutates":false,"name":["balance_of"],"payable":false,"returnType":{"displayName":["Balance"],"type":1},"selector":"0x56e929b2"},In api, can call by:bob_balance=api.balance_of(bob,bob.ss58_address)logging.info(f'transfer bob_balance{bob_balance}')ContractObserveris used to listen contracts eventsContractObserver can observer events for a contract:substrate=SubstrateInterface(url="ws://127.0.0.1:9944",type_registry_preset='canvas')contract_metadata=ContractMetadata.create_from_file(metadata_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.json'),substrate=substrate)observer=ContractObserver("0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48",contract_metadata,substrate)# for some handlers (callbacks)observer.scanEvents()The handler function can take the erc20 support as a example.Special case: ERC20 APIExcept react contract byContractAPI, developers could create the wrapper by themself to react with corresponding contract.py-contractcreate anERC20 APIas an example to show this.ERC20 api provide a wapper to erc20 contract exec, read and observer events, it can be a example for contracts api calling.# init apisubstrate=SubstrateInterface(url="ws://127.0.0.1:9944",type_registry_preset='canvas')contract_metadata=ContractMetadata.create_from_file(metadata_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.json'),substrate=substrate)alice=Keypair.create_from_uri('//Alice')bob=Keypair.create_from_uri('//Bob')# erc20 apierc20=ERC20.create_from_contracts(substrate=substrate,contract_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.wasm'),metadata_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.json'))# deplay a erc20 contracterc20.instantiate_with_code(alice,1000000*(10**15))# read total supplytotal_supply=erc20.totalSupply()# transfererc20.transfer_from(alice,from_acc=alice.ss58_address,to_acc=bob.ss58_address,amt=10000)erc20.transfer(alice,bob.ss58_address,10000)# get balancealice_balance=erc20.balance_of(alice.ss58_address)# approveerc20.approve(alice,spender=bob.ss58_address,amt=10000)# get allowancealice_allowance=erc20.allowance(alice.ss58_address,bob.ss58_address)ERC20Observeris a event observer for erc20 contract:observer=ERC20Observer.create_from_address(substrate=substrate,contract_address=contract_address,metadata_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.json'))defon_transfer(num,evt):logging.info("on_transfer in{}:{}{}{}".format(num,evt['from'],evt['to'],evt['value']))defon_approval(num,evt):logging.info("on_approval in{}:{}{}{}".format(num,evt['owner'],evt['spender'],evt['value']))observer.scanEvents(on_transfer=on_transfer,on_approval=on_approval)Observer For ContractsContractObserveris a observer to listen events by contract with a given address:observer=ContractObserver.create_from_address(substrate=substrate,contract_address='contract_address',metadata_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.json'))defon_transfer(num,evt):logging.info("on_transfer in{}:{}{}{}".format(num,evt['from'],evt['to'],evt['value']))defon_approval(num,evt):logging.info("on_approval in{}:{}{}{}".format(num,evt['owner'],evt['spender'],evt['value']))observer.scanEvents(from_num,to_num,{'Transfer':on_transfer,'Approve':on_approval})handlersis a hander dictionary by name to hander function.Unittest Node EnvironmentPatractPy can support write contract unittest by node environment.At First We need installeuropa.frompatractinterface.contracts.erc20importERC20frompatractinterface.unittest.envimportSubstrateTestEnvclassUnittestEnvTest(unittest.TestCase):@classmethoddefsetUp(cls):# start env or use canvas for a 6s blockcls.env=SubstrateTestEnv.create_europa(port=39944)cls.env.start_node()cls.api=SubstrateInterface(url=cls.env.url(),type_registry_preset=cls.env.typ(),type_registry=cls.env.types())cls.alice=Keypair.create_from_uri('//Alice')cls.bob=Keypair.create_from_uri('//Bob')cls.erc20=ERC20.create_from_contracts(substrate=cls.substrate,contract_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.wasm'),metadata_file=os.path.join(os.path.dirname(__file__),'constracts','ink','erc20.json'))cls.erc20.instantiate_with_code(alice,1000000*(10**15))returndeftearDown(cls):cls.env.stop_node()deftest_transfer(self):self.erc20.transfer_from(alice,from_acc=alice.ss58_address,to_acc=bob.ss58_address,amt=10000)# some more test caseif__name__=='__main__':unittest.main()By example, we can use python to write testcase for some complex logics, byeuropa, we can test the contracts for python scripts.