polkascan / py-substrate-interface

Python Substrate Interface
https://polkascan.github.io/py-substrate-interface/
Apache License 2.0
239 stars 111 forks source link

Unable to parse contract events #365

Closed deep-ink-ventures closed 7 months ago

deep-ink-ventures commented 7 months ago

I am facing multiple problems on parsing events.

  1. Every now and then the event data is bytes.

That is not happen all the times and so far I think it only occurs on u32, but I am not sure. The event data is then a str, e.g. \x00\x01\x00\x00\x00 that I then have to convert via bytes(x, 'utf-8')) in order to feed it to ScaleBytes.

It's hacky but seems to work, however, more seriously:

  1. NotImplementedError: Decoder class for "ink::<source/hash from json goes here>::0" not found is happening all the time

I'm not able at all to parse events at all. First of all, the Events are within the ContractEmitted part of the dict, not ContractExecution as mentioned in the str docs.

Is there an example on how to actually do this? I'm fetching a block and want to parse the event. I have the json metadata and can instantiate a contract code with is.

arjanz commented 7 months ago

What is the definition of type with id: 0 in the metadata.json?

Also, which version of Ink! is in language property?

Is there a way for me to reproduce this with the metadata.json and an ws endpoint where the contract is deployed (or WASM code)? Then I can do some debugging for you.

arjanz commented 7 months ago

I'm not able at all to parse events at all. First of all, the Events are within the ContractEmitted part of the dict, not ContractExecution as mentioned in the str docs.

This is the part of the code where the bytes of the ContractEmitted get decoded into a ContractEvent ScaleType.

deep-ink-ventures commented 7 months ago

Thanks!

Yes, you can do that here: https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fnode.genesis-dao.org#/explorer

https://github.com/deep-ink-ventures/genesis-dao-service/blob/main/wasm/dao_asset_contract.json is the metadata. https://github.com/deep-ink-ventures/genesis-dao-service/blob/main/wasm/dao_asset_contract.wasm is the wasm https://github.com/deep-ink-ventures/genesis-dao-node/blob/main/contracts/extensions/dao-assets-contract/src/lib.rs is creating the problem

I use this code, everything is setup:

            contract_code = ContractCode.create_from_contract_files(
                wasm_file=wasm_path,
                metadata_file=json_path,
                substrate=substrate_service.substrate_interface,
            )

            data = contract_code.metadata.generate_constructor_data(name="new", args={"asset_id": 1})

            call = .substrate_interface.compose_call(
                call_module="Contracts",
                call_function="instantiate_with_code",
                call_params={
                    "value": 0,
                    "gas_limit": {"ref_time": 2599000000, "proof_size": 1199038364791120855},
                    "storage_deposit_limit": None,
                    "code": "0x{}".format(contract_code.wasm_bytes.hex()),
                    "data": data.to_hex(),
                    "salt": uuid4().hex,
                },
            )
deep-ink-ventures commented 7 months ago

I'm not able at all to parse events at all. First of all, the Events are within the ContractEmitted part of the dict, not ContractExecution as mentioned in the str docs.

This is the part of the code where the bytes of the ContractEmitted get decoded into a ContractEvent ScaleType.

This is for parsing the receipt, isn't it? I want to parse events.

arjanz commented 7 months ago

I'm not able at all to parse events at all. First of all, the Events are within the ContractEmitted part of the dict, not ContractExecution as mentioned in the str docs.

This is the part of the code where the bytes of the ContractEmitted get decoded into a ContractEvent ScaleType.

This is for parsing the receipt, isn't it? I want to parse events.

This is for the contract events. The triggered events of an (contract call) extrinsic are part of the receipt

arjanz commented 7 months ago

Ok I tested a deployment and the processing of contract events. It was successful in the end, but did found a bug when creating a contract receipt from an on-chain extrinsic (which can be found here: https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fnode.genesis-dao.org#/explorer/query/230) .

First I deployed basically with the example found in the docs, I didn't encounter much problems:

# Upload WASM code
code = ContractCode.create_from_contract_files(
    metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'dao_asset_contract.json'),
    wasm_file=os.path.join(os.path.dirname(__file__), 'assets', 'dao_asset_contract.wasm'),
    substrate=substrate
)

# Deploy contract
print('Deploy contract...')
contract = code.deploy(
    keypair=keypair,
    constructor="new",
    args={'asset_id': 1},
    value=0,
    gas_limit={'ref_time': 2599000000, 'proof_size': 1199038364791120855},
    upload_code=True
)

print(f'✅ Deployed @ {contract.contract_address}')

Then I retrieved the extrinsic on-chain with the identifier and tried to output the contract events. There I encountered a bug, the extrinsic index is not passed on successfully and had to manually set the extrinsic hash.

# retrieve extrinsic on-chain previously deployed on 
receipt = substrate.retrieve_extrinsic_by_identifier("230-1")

contract_metadata = ContractMetadata.create_from_file(
    os.path.join(os.path.dirname(__file__), 'assets', 'dao_asset_contract.json'),
    substrate=substrate
)

contract_receipt = ContractExecutionReceipt.create_from_extrinsic_receipt(receipt, contract_metadata)
# bug found: exintric index is not passed to contract receipt, must be set manually
contract_receipt.extrinsic_hash = '0x96c7552c6711e48a6fb8793dd22586dadc7ac32c3d7fff09fc8c3c3fcf871e97'

print(contract_receipt.contract_events)

I'll make a note for this bug.

When you perform a contract call, this should all work fine btw, for example:

# Read current value
result = contract.read(keypair, 'get')
print('Current value of "get":', result.contract_result_data)

# Do a gas estimation of the message
gas_predit_result = contract.read(keypair, 'flip')

print('Result of dry-run: ', gas_predit_result.value)
print('Gas estimate: ', gas_predit_result.gas_required)

# Do the actual call
print('Executing contract call...')
contract_receipt = contract.exec(keypair, 'flip', args={

}, gas_limit=gas_predit_result.gas_required)

if contract_receipt.is_success:
    print(f'Events triggered in contract: {contract_receipt.contract_events}')
else:
    print(f'Error message: {contract_receipt.error_message}')
deep-ink-ventures commented 7 months ago

I fundamentally do not understand what you are doing here.

I have an event listener via the get_block() method and want to parse the event contained within. I don't want to look at receipts nor doing a call.

Can you enlighten me what I'm missing here?

arjanz commented 7 months ago

Ok gotcha, you are trying to observe and decode all contract events as they occur per block. I made a event subscription example, hope this helps!

The tricky part is to extract the bytes from the data attribute, this is currently indeed a bit cumbersome. obj.value is the 'human readable' version of the data and seems not always consistent. obj.value_object retains original type information.

import os

from scalecodec import ScaleBytes
from substrateinterface import SubstrateInterface, ContractEvent, ContractMetadata

substrate = SubstrateInterface(url="ws://127.0.0.1:9944")

contract_metadata = ContractMetadata.create_from_file(
            metadata_file=os.path.join(os.path.dirname(__file__), 'assets', 'metadata.json'),
            substrate=substrate
        )

def subscription_handler(storage_key, updated_obj, update_nr, subscription_id):
    # print(f"Update for {storage_key}: {updated_obj.value}")
    for event in updated_obj.value_object:
        if event.value['module_id'] == 'Contracts':
            print (f". Contract related event: {event.value['event_id']}: {event.value['attributes']}")
            if event.value['event_id'] == 'ContractEmitted':
                # Trying to decode Contract event:
                contract_event_obj = ContractEvent(
                    data=ScaleBytes(event.value_object['event'][1][1]['data'].value_object),
                    runtime_config=substrate.runtime_config,
                    contract_metadata=contract_metadata
                )
                print('* Event in Contract triggered:', contract_event_obj.decode())

# Storage keys to track
storage_keys = [
    substrate.create_storage_key(
        "System", "Events"
    ),
]

result = substrate.subscribe_storage(
    storage_keys=storage_keys, subscription_handler=subscription_handler
)
deep-ink-ventures commented 7 months ago

Yay! It works. Thank you.