Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New runtime interfaces #357

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions examples/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@
substrate.register_extension(SubstrateNodeExtension(max_block_range=100))

# Search for block number corresponding a specific datetime
block_datetime = datetime(2022, 1, 1, 0, 0, 0)
block_datetime = datetime(2022, 12, 31, 0, 0, 0)
block_number = substrate.extensions.search_block_number(block_datetime=block_datetime)
print(f'Block number for {block_datetime}: #{block_number}')
block_hash = substrate.get_block_hash(block_number)

# account_info = substrate.runtime.
# exit()
account_info = substrate.runtime.at(block_hash).pallet("System").storage("Account").get("13GnsRKEXCAYLJNScBEDj7rHTXkkHAVTj5QMNp6rnyGuTAVN")

def format_balance(amount: int):
amount = format(amount / 10**substrate.properties.get('tokenDecimals', 0), ".15g")
return f"{amount} {substrate.properties.get('tokenSymbol', 'UNIT')}"

balance = (account_info.value["data"]["free"] + account_info.value["data"]["reserved"])

print(f"Balance @ {block_number}: {format_balance(balance)}")

exit()

# Returns all `Balances.Transfer` events from the last 30 blocks
events = substrate.extensions.filter_events(pallet_name="Balances", event_name="Transfer", block_start=-30)
Expand Down
168 changes: 17 additions & 151 deletions substrateinterface/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@
from scalecodec.type_registry import load_type_registry_preset
from scalecodec.updater import update_type_registries
from .extensions import Extension
from .interfaces import ExtensionInterface
from .interfaces import ExtensionInterface, RuntimeInterface, QueryMapResult, ChainInterface, BlockInterface, \
ContractInterface

from .storage import StorageKey

from .exceptions import SubstrateRequestException, ConfigurationError, StorageFunctionNotFound, BlockNotFound, \
ExtrinsicNotFound, ExtensionCallNotFound
from .constants import *
from .keypair import Keypair, KeypairType, MnemonicLanguageCode
from .utils.ss58 import ss58_decode, ss58_encode, is_valid_ss58_address, get_ss58_format

from .keypair import Keypair
from .utils.ss58 import ss58_decode, ss58_encode, is_valid_ss58_address

__all__ = ['SubstrateInterface', 'ExtrinsicReceipt', 'logger']

Expand Down Expand Up @@ -157,8 +157,11 @@ def __init__(self, url=None, websocket=None, ss58_format=None, type_registry=Non
'rpc_methods': None
}

# Initialize extension interface
# Initialize interfaces
self.runtime = RuntimeInterface(self)
self.block = BlockInterface(self)
self.extensions = ExtensionInterface(self)
self.contract = ContractInterface(self)

self.session = requests.Session()

Expand Down Expand Up @@ -756,7 +759,7 @@ def query_map(self, module: str, storage_function: str, params: Optional[list] =

Returns
-------
QueryMapResult
substrateinterface.interfaces.QueryMapResult
"""

if block_hash is None:
Expand Down Expand Up @@ -976,88 +979,16 @@ def subscription_handler(obj, update_nr, subscription_id):
# Check requirements
if callable(subscription_handler):
raise ValueError("Subscriptions can only be registered for current state; block_hash cannot be set")
else:
# Retrieve chain tip
block_hash = self.get_chain_head()

if params is None:
params = []

self.init_runtime(block_hash=block_hash)

if module == 'Substrate':
# Search for 'well-known' storage keys
return self.__query_well_known(storage_function, block_hash)

# Search storage call in metadata
metadata_pallet = self.metadata.get_metadata_pallet(module)

if not metadata_pallet:
raise StorageFunctionNotFound(f'Pallet "{module}" not found')

storage_item = metadata_pallet.get_storage_function(storage_function)

if not metadata_pallet or not storage_item:
raise StorageFunctionNotFound(f'Storage function "{module}.{storage_function}" not found')

# SCALE type string of value
param_types = storage_item.get_params_type_string()
value_scale_type = storage_item.get_value_type_string()

if len(params) != len(param_types):
raise ValueError(f'Storage function requires {len(param_types)} parameters, {len(params)} given')

if raw_storage_key:
storage_key = StorageKey.create_from_data(
data=raw_storage_key, pallet=module, storage_function=storage_function,
value_scale_type=value_scale_type, metadata=self.metadata, runtime_config=self.runtime_config
)
else:

storage_key = StorageKey.create_from_storage_function(
module, storage_function, params, runtime_config=self.runtime_config, metadata=self.metadata
)

if callable(subscription_handler):
raise NotImplementedError()

# Wrap subscription handler to discard storage key arg
def result_handler(storage_key, updated_obj, update_nr, subscription_id):
return subscription_handler(updated_obj, update_nr, subscription_id)

return self.subscribe_storage([storage_key], result_handler)

else:

if self.supports_rpc_method('state_getStorageAt'):
response = self.rpc_request("state_getStorageAt", [storage_key.to_hex(), block_hash])
else:
response = self.rpc_request("state_getStorage", [storage_key.to_hex(), block_hash])

if 'error' in response:
raise SubstrateRequestException(response['error']['message'])

if 'result' in response:
if value_scale_type:

if response.get('result') is not None:
query_value = response.get('result')
elif storage_item.value['modifier'] == 'Default':
# Fallback to default value of storage function if no result
query_value = storage_item.value_object['default'].value_object
else:
# No result is interpreted as an Option<...> result
value_scale_type = f'Option<{value_scale_type}>'
query_value = storage_item.value_object['default'].value_object

obj = self.runtime_config.create_scale_object(
type_string=value_scale_type,
data=ScaleBytes(query_value),
metadata=self.metadata
)
obj.decode()
obj.meta_info = {'result_found': response.get('result') is not None}
if params is None:
params = []

return obj
return self.runtime.at(block_hash=block_hash).pallet(module).storage(storage_function).get(
*params, raw_storage_key=raw_storage_key
)

def __query_well_known(self, name: str, block_hash: str) -> ScaleType:
"""
Expand Down Expand Up @@ -1387,20 +1318,10 @@ def compose_call(self, call_module: str, call_function: str, call_params: dict =
if call_params is None:
call_params = {}

self.init_runtime(block_hash=block_hash)

call = self.runtime_config.create_scale_object(
type_string='Call', metadata=self.metadata
return self.runtime.at(block_hash=block_hash).pallet(call_module).call(call_function).create(
**call_params
)

call.encode({
'call_module': call_module,
'call_function': call_function,
'call_args': call_params
})

return call

def get_account_nonce(self, account_address) -> int:
"""
Returns current nonce for given account address
Expand Down Expand Up @@ -3399,58 +3320,3 @@ def get(self, name):
return self[name]


class QueryMapResult:

def __init__(self, records: list, page_size: int, module: str = None, storage_function: str = None,
params: list = None, block_hash: str = None, substrate: SubstrateInterface = None,
last_key: str = None, max_results: int = None, ignore_decoding_errors: bool = False):
self.current_index = -1
self.records = records
self.page_size = page_size
self.module = module
self.storage_function = storage_function
self.block_hash = block_hash
self.substrate = substrate
self.last_key = last_key
self.max_results = max_results
self.params = params
self.ignore_decoding_errors = ignore_decoding_errors
self.loading_complete = False

def retrieve_next_page(self, start_key) -> list:
if not self.substrate:
return []

result = self.substrate.query_map(module=self.module, storage_function=self.storage_function,
params=self.params, page_size=self.page_size, block_hash=self.block_hash,
start_key=start_key, max_results=self.max_results,
ignore_decoding_errors=self.ignore_decoding_errors)

# Update last key from new result set to use as offset for next page
self.last_key = result.last_key

return result.records

def __iter__(self):
self.current_index = -1
return self

def __next__(self):
self.current_index += 1

if self.max_results is not None and self.current_index >= self.max_results:
self.loading_complete = True
raise StopIteration

if self.current_index >= len(self.records) and not self.loading_complete:
# try to retrieve next page from node
self.records += self.retrieve_next_page(start_key=self.last_key)

if self.current_index >= len(self.records):
self.loading_complete = True
raise StopIteration

return self.records[self.current_index]

def __getitem__(self, item):
return self.records[item]
Loading
Loading