diff --git a/app/__init__.py b/app/__init__.py index 1e96cc6..e69de29 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,2 +0,0 @@ -from . import setup, cartridge, replay, scoreboard -# TODO: use settings file instead of init diff --git a/app/cartridge.py b/app/cartridge.py index 84c97f6..dca571b 100644 --- a/app/cartridge.py +++ b/app/cartridge.py @@ -10,10 +10,12 @@ from cartesi.abi import String, Bytes, Bytes32, UInt from cartesapp.storage import Entity, helpers, seed -from cartesapp.manager import query, mutation, get_metadata, event, output, add_output, emit_event, contract_call +from cartesapp.context import get_metadata +from cartesapp.input import query, mutation +from cartesapp.output import event, output, add_output, emit_event, contract_call from .riv import riv_get_cartridge_info, riv_get_cartridge_screenshot, riv_get_cartridges_path, riv_get_cover, riv_get_cartridge_outcard -from .setup import AppSettings +from .settings import AppSettings LOGGER = logging.getLogger(__name__) @@ -26,7 +28,7 @@ class Cartridge(Entity): id = helpers.PrimaryKey(str, 64) name = helpers.Required(str, index=True, unique=True) - user_address = helpers.Required(str, 66) + user_address = helpers.Required(str, 42) info = helpers.Optional(helpers.Json, lazy=True) created_at = helpers.Required(int) cover = helpers.Optional(bytes, lazy=True) diff --git a/app/common.py b/app/common.py new file mode 100644 index 0000000..a9ac033 --- /dev/null +++ b/app/common.py @@ -0,0 +1,24 @@ +import os +from enum import Enum + +class ScoreType(Enum): + default = 0 + scoreboard = 1 + tournament = 2 + + +class GameplayHash: + cartridge_replays = {} + def __new__(cls): + return cls + + @classmethod + def add(cls, cartridge_id, replay_hash): + if cls.cartridge_replays.get(cartridge_id) is None: cls.cartridge_replays[cartridge_id] = {} + cls.cartridge_replays[cartridge_id][replay_hash] = True + + @classmethod + def check(cls, cartridge_id, replay_hash): + return cls.cartridge_replays.get(cartridge_id) is None \ + or cls.cartridge_replays[cartridge_id].get(replay_hash) is None \ + or cls.cartridge_replays[cartridge_id][replay_hash] == False diff --git a/app/replay.py b/app/replay.py index 69de09a..edc894b 100644 --- a/app/replay.py +++ b/app/replay.py @@ -9,12 +9,15 @@ from cartesi.abi import String, Bytes, Bytes32, Int, UInt -from cartesapp.storage import helpers # TODO: create repo to avoid this relative import hassle -from cartesapp.manager import mutation, get_metadata, add_output, event, emit_event, contract_call # TODO: create repo to avoid this relative import hassle +from cartesapp.storage import helpers +from cartesapp.context import get_metadata +from cartesapp.input import mutation +from cartesapp.output import add_output, event, emit_event, contract_call from cartesapp.utils import bytes2str -from .setup import AppSettings, ScoreType, GameplayHash +from .settings import AppSettings from .riv import replay_log +from .common import ScoreType, GameplayHash LOGGER = logging.getLogger(__name__) diff --git a/app/scoreboard.py b/app/scoreboard.py index d9bffdf..9529c93 100644 --- a/app/scoreboard.py +++ b/app/scoreboard.py @@ -10,12 +10,15 @@ from cartesi.abi import String, Bytes, Bytes32, Int, UInt from cartesapp.storage import Entity, helpers, seed -from cartesapp.manager import mutation, query, get_metadata, output, add_output, event, emit_event, contract_call +from cartesapp.context import get_metadata +from cartesapp.input import mutation, query +from cartesapp.output import output, add_output, event, emit_event, contract_call from cartesapp.utils import hex2bytes, str2bytes, bytes2str -from .setup import AppSettings, ScoreType, GameplayHash +from .settings import AppSettings from .riv import replay_log, riv_get_cartridge_outcard from .cartridge import Cartridge +from .common import ScoreType, GameplayHash LOGGER = logging.getLogger(__name__) @@ -28,7 +31,7 @@ class Scoreboard(Entity): id = helpers.PrimaryKey(str, 64) name = helpers.Required(str, index=True, unique=True) cartridge_id = helpers.Required(str, 64, index=True) - created_by = helpers.Required(str, 66) + created_by = helpers.Required(str, 42) created_at = helpers.Required(int) args = helpers.Optional(str) in_card = helpers.Optional(bytes) @@ -38,7 +41,7 @@ class Scoreboard(Entity): class Score(Entity): id = helpers.PrimaryKey(int, auto=True) - user_address = helpers.Required(str, 66, index=True) + user_address = helpers.Required(str, 42, index=True) timestamp = helpers.Required(int) score = helpers.Required(int) scoreboard = helpers.Required(Scoreboard, index=True) diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..69a2f34 --- /dev/null +++ b/app/settings.py @@ -0,0 +1,17 @@ +# App Framework settings + +# Files with definitions to import +FILES = ['setup','cartridge','replay','scoreboard'] # * Required + +# Index outputs in inspect indexer queries +INDEX_OUTPUTS = True # Defaul: False + +ENABLE_DAPP_RELAY = False # Defaul: False + +ENABLE_WALLET = False # Defaul: False (required to set ENABLE_DAPP_RELAY) + + +class AppSettings: + rivemu_path = None + cartridges_path = "cartridges" + scoreboard_ttl = 7776000 # 90 days diff --git a/app/setup.py b/app/setup.py index a661df1..7639af0 100644 --- a/app/setup.py +++ b/app/setup.py @@ -1,40 +1,8 @@ import os -from enum import Enum -from cartesapp.manager import setup, setting - -# TODO: use settings file instead of init -@setting() -class FrameworkSettings: - index_outputs = True - -class AppSettings: - rivemu_path = None - cartridges_path = "cartridges" - scoreboard_ttl = 7776000 # 90 days +from cartesapp.manager import setup +from .settings import AppSettings @setup() def setup_rivemu(): AppSettings.rivemu_path = os.getenv('RIVEMU_PATH') - -class ScoreType(Enum): - default = 0 - scoreboard = 1 - tournament = 2 - - -class GameplayHash: - cartridge_replays = {} - def __new__(cls): - return cls - - @classmethod - def add(cls, cartridge_id, replay_hash): - if cls.cartridge_replays.get(cartridge_id) is None: cls.cartridge_replays[cartridge_id] = {} - cls.cartridge_replays[cartridge_id][replay_hash] = True - - @classmethod - def check(cls, cartridge_id, replay_hash): - return cls.cartridge_replays.get(cartridge_id) is None \ - or cls.cartridge_replays[cartridge_id].get(replay_hash) is None \ - or cls.cartridge_replays[cartridge_id][replay_hash] == False diff --git a/cartesapp/context.py b/cartesapp/context.py new file mode 100644 index 0000000..50dac52 --- /dev/null +++ b/cartesapp/context.py @@ -0,0 +1,50 @@ + +from cartesi import Rollup, RollupData, RollupMetadata + + +### +# Context + +class Context(object): + rollup: Rollup | None = None + metadata: RollupMetadata | None = None + module: str | None = None + n_reports: int = 0 + n_notices: int = 0 + n_vouchers: int = 0 + configs = None + dapp_address: str | None = None + + + def __new__(cls): + return cls + + @classmethod + def set_context(cls, rollup: Rollup, metadata: RollupMetadata, module: str, **kwargs): + cls.rollup = rollup + cls.metadata = metadata + cls.module = module + cls.n_reports = 0 + cls.n_notices = 0 + cls.n_vouchers = 0 + cls.configs = kwargs + + @classmethod + def clear_context(cls): + cls.rollup = None + cls.metadata = None + cls.module = None + cls.n_reports: 0 + cls.n_notices = 0 + cls.n_vouchers = 0 + cls.configs = None + + +### +# Helpers + +def get_metadata() -> RollupMetadata: + return Context.metadata + +def get_dapp_address() -> str | None: + return Context.dapp_address diff --git a/cartesapp/indexer/__init__.py b/cartesapp/indexer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cartesapp/indexer/output_index.py b/cartesapp/indexer/output_index.py new file mode 100644 index 0000000..fe7cf28 --- /dev/null +++ b/cartesapp/indexer/output_index.py @@ -0,0 +1,118 @@ +from pydantic import BaseModel +from typing import Optional, List + +from cartesapp.storage import Entity, helpers +from cartesapp.input import query +from cartesapp.output import output, add_output + + +### +# Indexer model and methods + +class Output(Entity): + id = helpers.PrimaryKey(int, auto=True) + output_type = helpers.Required(str) # helpers.Required(OutputType) + msg_sender = helpers.Required(str, 42, lazy=True, index=True) + block_number = helpers.Required(int, lazy=True) + timestamp = helpers.Required(int, lazy=True, index=True) + epoch_index = helpers.Required(int, lazy=True) + input_index = helpers.Required(int) + output_index = helpers.Required(int) + output_module = helpers.Required(str) + output_class = helpers.Required(str) + tags = helpers.Set("OutputTag") + +class OutputTag(Entity): + id = helpers.PrimaryKey(int, auto=True) + name = helpers.Required(str, index=True) + output = helpers.Required(Output, index=True) + + +def add_output_index(metadata,output_type,output_index,output_module,output_class,tags=None): + o = Output( + output_type = output_type.name.lower(), + output_class = output_class, + output_module = output_module, + msg_sender = metadata.msg_sender.lower(), + block_number = metadata.block_number, + timestamp = metadata.timestamp, + epoch_index = metadata.epoch_index, + input_index = metadata.input_index, + output_index = output_index + ) + if tags is not None: + for tag in tags: + t = OutputTag( + name = tag, + output = o + ) + +def get_output_indexes(**kwargs): + tags = kwargs.get('tags') + + output_query = Output.select() + + tag_query = OutputTag.select() + + if tags is not None and len(tags) > 0: + tag_query = tag_query.filter(lambda t: t.name in tags) + + if kwargs.get('module') is not None: + output_query = output_query.filter(lambda o: o.output_module == kwargs.get('module').lower()) + if kwargs.get('output_type') is not None: + output_query = output_query.filter(lambda o: o.output_type == kwargs.get('output_type').lower()) + if kwargs.get('msg_sender') is not None: + output_query = output_query.filter(lambda o: o.msg_sender == kwargs.get('msg_sender').lower()) + if kwargs.get('timestamp_gte') is not None: + output_query = output_query.filter(lambda o: o.timestamp >= kwargs.get('timestamp_gte')) + if kwargs.get('timestamp_lte') is not None: + output_query = output_query.filter(lambda o: o.timestamp <= kwargs.get('timestamp_lte')) + if kwargs.get('input_index') is not None: + output_query = output_query.filter(lambda o: o.input_index == kwargs.get('input_index')) + + if tags is not None and len(tags) > 0: + query = helpers.distinct( + [o.output_type,o.output_module,o.output_class,o.input_index,o.output_index] + for o in output_query for t in tag_query if t.output == o and helpers.count(t) == len(tags) + ) + else: + query = helpers.distinct( + [o.output_type,o.output_module,o.output_class,o.input_index,o.output_index] + for o in output_query for t in tag_query if t.output == o + ) + + return query.fetch() + + + + +class IndexerPayload(BaseModel): + tags: Optional[List[str]] + output_type: Optional[str] + msg_sender: Optional[str] + timestamp_gte: Optional[int] + timestamp_lte: Optional[int] + module: Optional[str] + input_index: Optional[int] + +class OutputIndex(BaseModel): + output_type: str + module: str + class_name: str + input_index: int + output_index: int + + +@output(module_name='indexer') +class IndexerOutput(BaseModel): + data: List[OutputIndex] + +@query(module_name='indexer') +def indexer_query(payload: IndexerPayload) -> bool: + out = get_output_indexes(**payload.dict()) + + output_inds = [OutputIndex(output_type=r[0],module=r[1],class_name=r[2],input_index=r[3],output_index=r[4]) for r in out] + + add_output(IndexerOutput(data=output_inds)) + + return True diff --git a/cartesapp/input.py b/cartesapp/input.py new file mode 100644 index 0000000..1a72857 --- /dev/null +++ b/cartesapp/input.py @@ -0,0 +1,150 @@ +import logging +from typing import Optional, List, get_type_hints +import traceback + +from cartesi import Rollup, RollupData, RollupMetadata, URLParameters, abi + +from .storage import helpers +from .context import Context +from .output import add_output + +LOGGER = logging.getLogger(__name__) + +# Query +class Query: + queries = [] + configs = {} + def __new__(cls): + return cls + + @classmethod + def add(cls, func, **kwargs): + cls.queries.append(func) + func_name = func.__name__ + module_name = func.__module__.split('.')[0] + cls.configs[f"{module_name}.{func_name}"] = kwargs + +def query(**kwargs): + def decorator(func): + Query.add(func,**kwargs) + return func + return decorator + +splittable_query_params = {"part":(int,None)} + +# Mutation +class Mutation: + mutations = [] + configs = {} + def __new__(cls): + return cls + + @classmethod + def add(cls, func, **kwargs): + cls.mutations.append(func) + func_name = func.__name__ + module_name = func.__module__.split('.')[0] + cls.configs[f"{module_name}.{func_name}"] = kwargs + +# TODO: decorator params to allow chunked and compressed mutations +def mutation(**kwargs): + if kwargs.get('chunk') is not None: + LOGGER.warning("Chunking inputs is not implemented yet") + if kwargs.get('compress') is not None: + LOGGER.warning("Compressing inputs is not implemented yet") + if kwargs.get('sender_address') is not None: + LOGGER.warning("Sender address filtering is not implemented yet") + def decorator(func): + Mutation.add(func,**kwargs) + return func + return decorator + + +### +# Helpers + +def _make_query(func,model,has_param,module,**func_configs): + @helpers.db_session + def query(rollup: Rollup, params: URLParameters) -> bool: + try: + ctx = Context + # TODO: accept abi encode or json (for larger post requests, configured in settings) + # Decoding url parameters + param_list = [] + if has_param: + hints = get_type_hints(model) + fields = [] + values = [] + model_fields = model.__fields__.keys() + for k in model_fields: + if k in params.query_params: + field_str = str(hints[k]) + if field_str.startswith('typing.List') or field_str.startswith('typing.Optional[typing.List'): + fields.append(k) + values.append(params.query_params[k]) + else: + fields.append(k) + values.append(params.query_params[k][0]) + if k in params.path_params: + fields.append(k) + values.append(params.path_params[k]) + param_list.append(model.parse_obj(dict(zip(fields, values)))) + + extended_model = func_configs.get("extended_model") + if extended_model is not None: + extended_hints = get_type_hints(extended_model) + for k in list(set(extended_model.__fields__.keys()).difference(model_fields)): + if k in params.query_params: + field_str = str(extended_hints[k]) + if field_str.startswith('typing.List') or field_str.startswith('typing.Optional[typing.List'): + fields.append(k) + values.append(params.query_params[k]) + else: + fields.append(k) + values.append(params.query_params[k][0]) + func_configs["extended_params"] = extended_model.parse_obj(dict(zip(fields, values))) + + ctx.set_context(rollup,None,module,**func_configs) + res = func(*param_list) + except Exception as e: + msg = f"Error: {e}" + LOGGER.error(msg) + if logging.root.level <= logging.DEBUG: + traceback.print_exc() + add_output(msg) + res = False + finally: + helpers.rollback() + ctx.clear_context() + return res + return query + +def _make_mut(func,model,has_param,module, **kwargs): + @helpers.db_session + def mut(rollup: Rollup, data: RollupData) -> bool: + try: + ctx = Context + ctx.set_context(rollup,data.metadata,module,**kwargs) + payload = data.bytes_payload()[(4 if kwargs.get('has_header') else 0):] + param_list = [] + decode_params = { + "data":payload, + "model":model + } + is_packed = kwargs.get('packed') + if is_packed is not None: decode_params["packed"] = is_packed + if has_param: + param_list.append(abi.decode_to_model(**decode_params)) + res = func(*param_list) + except Exception as e: + msg = f"Error: {e}" + LOGGER.error(msg) + if logging.root.level <= logging.DEBUG: + traceback.print_exc() + add_output(msg,tags=['error']) + res = False + finally: + if not res: helpers.rollback() + ctx.clear_context() + return res + return mut diff --git a/cartesapp/manager.py b/cartesapp/manager.py index a5e1c9a..ae2451b 100644 --- a/cartesapp/manager.py +++ b/cartesapp/manager.py @@ -2,29 +2,30 @@ import logging import importlib from inspect import getmembers, isfunction, signature -import sys, getopt -from typing import Optional, List, get_type_hints +from typing import Optional, List from pydantic import BaseModel, create_model -from Crypto.Hash import keccak -from enum import Enum -import json import traceback import typer from cartesi import DApp, Rollup, RollupData, RollupMetadata, ABIRouter, URLRouter, URLParameters, abi from cartesi.models import ABIFunctionSelectorHeader -from cartesi.abi import encode_model -from .storage import Storage, helpers, add_output_index, OutputType, get_output_indexes -from .utils import str2bytes, hex2bytes, bytes2hex -from .output import MAX_OUTPUT_SIZE, MAX_AGGREGATED_OUTPUT_SIZE, MAX_SPLITTABLE_OUTPUT_SIZE +from .storage import Storage, helpers +from .output import MAX_OUTPUT_SIZE, MAX_AGGREGATED_OUTPUT_SIZE, MAX_SPLITTABLE_OUTPUT_SIZE, Output +from .input import Query, Mutation, _make_mut, _make_query +from .setting import Setting LOGGER = logging.getLogger(__name__) +### +# Aux + class EmptyClass(BaseModel): pass +splittable_query_params = {"part":(int,None)} + ### # Manager @@ -50,16 +51,52 @@ def _import_apps(cls): if len(cls.modules_to_add) == 0: raise Exception("No modules detected") + add_dapp_relay = False + add_indexer_query = False + add_wallet = False for module_name in cls.modules_to_add: - importlib.import_module(module_name) + stg = importlib.import_module(f"{module_name}.settings") + if not hasattr(stg,'FILES'): + raise Exception(f"Module {module_name} has nothing to import (no FILES defined)") + + files_to_import = getattr(stg,'FILES') + if not isinstance(files_to_import, list) or len(files_to_import) == 0: + raise Exception(f"Module {module_name} has nothing to import (empty FILES list)") + + Setting.add(stg) + if not add_indexer_query and hasattr(stg,'INDEX_OUTPUTS') and getattr(stg,'INDEX_OUTPUTS'): + add_indexer_query = True + + if not add_dapp_relay and hasattr(stg,'ENABLE_DAPP_RELAY') and getattr(stg,'ENABLE_DAPP_RELAY'): + add_dapp_relay = True + + if not add_wallet and hasattr(stg,'ENABLE_WALLET') and getattr(stg,'ENABLE_WALLET'): + if not add_dapp_relay: + raise Exception(f"To enable wallet you should enable dapp relay") + add_dapp_relay = True + + + for f in files_to_import: + importlib.import_module(f"{module_name}.{f}") + + if add_indexer_query: + indexer_lib = importlib.import_module(f".indexer.output_index",package='cartesapp') + Output.add_output_index = indexer_lib.add_output_index + + if add_dapp_relay: + importlib.import_module(f"cartesapp.relay.dapp_relay") + + if add_dapp_relay: + importlib.import_module(f"cartesapp.wallet.dapp_wallet") @classmethod def _register_queries(cls, add_to_router=True): query_selectors = [] for func in Query.queries: func_name = func.__name__ - module_name = func.__module__.split('.')[0] - configs = Query.configs[f"{module_name}.{func_name}"] + original_module_name = func.__module__.split('.')[0] + configs = Query.configs[f"{original_module_name}.{func_name}"] + module_name = configs.get('module_name') if configs.get('module_name') is not None else original_module_name sig = signature(func) @@ -75,6 +112,10 @@ def _register_queries(cls, add_to_router=True): # using url router path = f"{module_name}/{func_name}" + path_params = configs.get('path_params') + if path_params is not None: + for p in path_params: + path = f"{path}/{'{'+p+'}'}" if path in query_selectors: raise Exception("Duplicate query selector") query_selectors.append(path) @@ -98,8 +139,9 @@ def _register_mutations(cls, add_to_router=True): mutation_selectors = [] for func in Mutation.mutations: func_name = func.__name__ - module_name = func.__module__.split('.')[0] - configs = Mutation.configs[f"{module_name}.{func_name}"] + original_module_name = func.__module__.split('.')[0] + configs = Mutation.configs[f"{original_module_name}.{func_name}"] + module_name = configs.get('module_name') if configs.get('module_name') is not None else original_module_name sig = signature(func) @@ -115,31 +157,31 @@ def _register_mutations(cls, add_to_router=True): # using abi router abi_types = abi.get_abi_types_from_model(model) - header = ABIFunctionSelectorHeader( - function=f"{module_name}.{func_name}", - argument_types=abi_types - ) - header_selector = header.to_bytes().hex() - if header_selector in mutation_selectors: - raise Exception("Duplicate mutation selector") - mutation_selectors.append(header_selector) + header = None + header_selector = None + no_header = configs.get('no_header') + has_header = no_header is None or not no_header + if has_header: + header = ABIFunctionSelectorHeader( + function=f"{module_name}.{func_name}", + argument_types=abi_types + ) + header_selector = header.to_bytes().hex() + if header_selector in mutation_selectors: + raise Exception("Duplicate mutation selector") + mutation_selectors.append(header_selector) + + func_configs = {'has_header':has_header} + if configs.get('packed'): func_configs['packed'] = configs['packed'] + cls.mutations_info[f"{module_name}.{func_name}"] = {"selector":header,"module":module_name,"method":func_name,"abi_types":abi_types,"model":model,"configs":configs} if add_to_router: - LOGGER.info(f"Adding mutation {module_name}.{func_name} selector={header_selector}, model={model.schema()}") - cls.abi_router.advance(header=header)(_make_mut(func,model,param is not None,module_name,**Mutation.configs[f"{module_name}.{func_name}"])) - - @classmethod - def _setup_settings(cls): - add_indexer_query = False - settings = Setting.settings - for module_name in settings: - settings_cls = settings[module_name] - if getattr(settings_cls,'index_outputs'): - add_indexer_query = True - break - if add_indexer_query: - output()(IndexerOutput) - query()(indexer_query) + LOGGER.info(f"Adding mutation {module_name}.{func_name} selector={header_selector}, model={model.__name__}") + advance_kwargs = {} + if has_header: advance_kwargs['header'] = header + msg_sender = configs.get('msg_sender') + if msg_sender is not None: advance_kwargs['msg_sender'] = msg_sender + cls.abi_router.advance(**advance_kwargs)(_make_mut(func,model,param is not None,module_name,**func_configs)) @classmethod def _run_setup_functions(cls): @@ -155,7 +197,6 @@ def run(cls): cls.dapp.add_router(cls.abi_router) cls.dapp.add_router(cls.url_router) cls._import_apps() - cls._setup_settings() cls._register_queries() cls._register_mutations() cls._run_setup_functions() @@ -165,18 +206,17 @@ def run(cls): @classmethod def generate_frontend_lib(cls, lib_path=None): cls._import_apps() - cls._setup_settings() cls._register_queries(False) cls._register_mutations(False) # generate lib from .template_frontend_generator import render_templates params = [ - {"indexer_query":indexer_query,"indexer_output":IndexerOutput}, Setting.settings, cls.mutations_info, cls.queries_info, Output.notices_info, Output.reports_info, + Output.vouchers_info, cls.modules_to_add] if lib_path is not None: params.append(lib_path) render_templates(*params) @@ -186,108 +226,6 @@ def create_frontend(cls): from .template_frontend_generator import create_frontend_structure create_frontend_structure() -### -# Singletons - -# Query -class Query: - queries = [] - configs = {} - def __new__(cls): - return cls - - @classmethod - def add(cls, func, **kwargs): - cls.queries.append(func) - func_name = func.__name__ - module_name = func.__module__.split('.')[0] - cls.configs[f"{module_name}.{func_name}"] = kwargs - -def query(**kwargs): - def decorator(func): - Query.add(func,**kwargs) - return func - return decorator - -splittable_query_params = {"part":(int,None)} - -# Mutation -class Mutation: - mutations = [] - configs = {} - def __new__(cls): - return cls - - @classmethod - def add(cls, func, **kwargs): - cls.mutations.append(func) - func_name = func.__name__ - module_name = func.__module__.split('.')[0] - cls.configs[f"{module_name}.{func_name}"] = kwargs - -# TODO: decorator params to allow chunked and compressed mutations -def mutation(**kwargs): - if kwargs.get('chunk') is not None: - LOGGER.warning("Chunking inputs is not implemented yet") - if kwargs.get('compress') is not None: - LOGGER.warning("Compressing inputs is not implemented yet") - if kwargs.get('sender_address') is not None: - LOGGER.warning("Sender address filtering is not implemented yet") - def decorator(func): - Mutation.add(func,**kwargs) - return func - return decorator - - -# Settings -class Setting: - settings = {} - def __new__(cls): - return cls - - @classmethod - def add(cls, klass): - cls.settings[klass.__module__.split('.')[0]] = klass - -def setting(**kwargs): - def decorator(klass): - Setting.add(klass) - return klass - return decorator - -class Context(object): - rollup: Rollup | None = None - metadata: RollupMetadata | None = None - module: str | None = None - n_reports: int = 0 - n_notices: int = 0 - n_vouchers: int = 0 - configs = None - - - def __new__(cls): - return cls - - @classmethod - def set_context(cls, rollup: Rollup, metadata: RollupMetadata, module: str, **kwargs): - cls.rollup = rollup - cls.metadata = metadata - cls.module = module - cls.n_reports = 0 - cls.n_notices = 0 - cls.n_vouchers = 0 - cls.configs = kwargs - - @classmethod - def clear_context(cls): - cls.rollup = None - cls.metadata = None - cls.module = None - cls.n_reports: 0 - cls.n_notices = 0 - cls.n_vouchers = 0 - cls.configs = None - class Setup: setup_functions = [] @@ -311,309 +249,6 @@ def decorator(func): return decorator -### -# Outputs - -class OutputFormat(Enum): - abi = 0 - packed_abi = 1 - json = 2 - -class Output: - notices_info = {} - reports_info = {} - vouchers_info = {} - def __new__(cls): - return cls - - @classmethod - def add_report(cls, klass): - module_name = klass.__module__.split('.')[0] - class_name = klass.__name__ - abi_types = [] # abi.get_abi_types_from_model(klass) - cls.reports_info[f"{module_name}.{class_name}"] = {"module":module_name,"class":class_name,"abi_types":abi_types,"model":klass} - - @classmethod - def add_notice(cls, klass): - module_name = klass.__module__.split('.')[0] - class_name = klass.__name__ - abi_types = abi.get_abi_types_from_model(klass) - cls.notices_info[f"{module_name}.{class_name}"] = {"module":module_name,"class":class_name,"abi_types":abi_types,"model":klass} - - @classmethod - def add_voucher(cls, klass): - module_name = klass.__module__.split('.')[0] - class_name = klass.__name__ - abi_types = abi.get_abi_types_from_model(klass) - cls.vouchers_info[f"{module_name}.{class_name}"] = {"module":module_name,"class":class_name,"abi_types":abi_types,"model":klass} - -def notice(**kwargs): - def decorator(klass): - Output.add_notice(klass) - return klass - return decorator - -event = notice - -def report(**kwargs): - def decorator(klass): - Output.add_report(klass) - return klass - return decorator - -output = report - -def voucher(**kwargs): - def decorator(klass): - Output.add_voucher(klass) - return klass - return decorator - -contract_call = voucher - -def get_metadata() -> RollupMetadata: - return Context.metadata - -def normalize_output(data,encode_format) -> [bytes, str]: - if isinstance(data, bytes): return data,'bytes' - if isinstance(data, int): data.to_bytes(32,byteorder='big'),'int' - if isinstance(data, str): - if data.startswith('0x'): return hex2bytes(data[2:]),'hex' - return str2bytes(data),'str' - class_name = f"{data.__module__.split('.')[0]}.{data.__class__.__name__}" - if isinstance(data, dict) or isinstance(data, list) or isinstance(data, tuple): - return str2bytes(json.dumps(data)),class_name - if issubclass(data.__class__,BaseModel): - if encode_format == OutputFormat.abi: return encode_model(data),class_name - if encode_format == OutputFormat.packed_abi: return encode_model(data,True),class_name - if encode_format == OutputFormat.json: return str2bytes(data.json(exclude_unset=True,exclude_none=True)),class_name - raise Exception("Invalid output format") - -def normalize_voucher(*kargs): - if len(kargs) == 1: - if isinstance(kargs[0], bytes): return kargs[0],'bytes' - if isinstance(kargs[0], str): return hex2bytes(kargs[0]),'hex' - raise Exception("Invalid voucher payload") - if len(kargs) == 2: - if not isinstance(kargs[0], str): raise Exception("Invalid voucher selector") - if not issubclass(kargs[1].__class__,BaseModel): raise Exception("Invalid voucher model") - - sig_hash = keccak.new(digest_bits=256) - sig_hash.update(kargs[0].encode('utf-8')) - - selector = sig_hash.digest()[:4] - data = abi.encode_model(kargs[1]) - - return selector+data,kargs[1].__class__.__name__ - # TODO: 3 is name, classes, and data, - # too many problems: how is model stored in index? how formats are defined: str or abi annotation? - raise Exception("Invalid number of arguments") - -def send_report(payload_data, **kwargs): - ctx = Context - # only one output to allow always chunking - if ctx.n_reports > 0: raise Exception("Can't add multiple reports") - - stg = Setting.settings.get(ctx.module) - - report_format = OutputFormat[getattr(stg,'report_format')] if hasattr(stg,'report_format') else OutputFormat.json - payload,class_name = normalize_output(payload_data,report_format) - - extended_params = ctx.configs.get("extended_params") - if extended_params is not None: - if ctx.metadata is None: # inspect - part = extended_params.part - payload_len = len(payload) - if payload_len > MAX_SPLITTABLE_OUTPUT_SIZE and part is not None: - if part >= 0: - startb = MAX_SPLITTABLE_OUTPUT_SIZE*(part) - endb = MAX_SPLITTABLE_OUTPUT_SIZE*(part+1) - payload = payload[startb:endb] - if endb < payload_len: payload += b'0' - - if len(payload) > MAX_AGGREGATED_OUTPUT_SIZE: - LOGGER.warn("Payload Data exceed maximum length. Truncating") - payload = payload[:MAX_AGGREGATED_OUTPUT_SIZE] - - # Always chunk if len > MAX_OUTPUT_SIZE - # if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum report length violation") - - tags = kwargs.get('tags') - add_idx = ctx.metadata is not None and stg is not None \ - and hasattr(stg,'index_outputs') and getattr(stg,'index_outputs') - - sent_bytes = 0 - while sent_bytes < len(payload): - inds = f" ({ctx.metadata.input_index}, {ctx.n_reports})" if ctx.metadata is not None else "" - top_bytes = sent_bytes + MAX_OUTPUT_SIZE - if top_bytes > len(payload): - top_bytes = len(payload) - - if add_idx: - splited_class_name = class_name.split('.')[-1] - LOGGER.debug(f"Adding index report{inds} {tags=}") - add_output_index(ctx.metadata,OutputType.report,ctx.n_reports,ctx.module,splited_class_name,tags) - - LOGGER.debug(f"Sending report{inds} {top_bytes - sent_bytes} bytes") - ctx.rollup.report(bytes2hex(payload[sent_bytes:top_bytes])) - ctx.n_reports += 1 - sent_bytes = top_bytes - -add_output = send_report - -def send_notice(payload_data, **kwargs): - ctx = Context - stg = Setting.settings.get(ctx.module) - - notice_format = OutputFormat[getattr(stg,'notice_format')] if hasattr(stg,'notice_format') else OutputFormat.abi - payload,class_name = normalize_output(payload_data,notice_format) - - if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum output length violation") - - tags = kwargs.get('tags') - - inds = f" ({ctx.metadata.input_index}, {ctx.n_notices})" if ctx.metadata is not None else "" - if ctx.metadata is not None and stg is not None and hasattr(stg,'index_outputs') and getattr(stg,'index_outputs'): - LOGGER.debug(f"Adding index notice{inds} {tags=}") - splited_class_name = class_name.split('.')[-1] - add_output_index(ctx.metadata,OutputType.notice,ctx.n_notices,ctx.module,splited_class_name,tags) - - LOGGER.debug(f"Sending notice{inds} {len(payload)} bytes") - ctx.rollup.notice(bytes2hex(payload)) - ctx.n_notices += 1 - -emit_event = send_notice - -def send_voucher(destination: str, *kargs, **kwargs): - payload,class_name = normalize_voucher() - - if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum output length violation") - - ctx = Context - stg = Setting.settings.get(ctx.module) - tags = kwargs.get('tags') - inds = f" ({ctx.metadata.input_index}, {ctx.n_vouchers})" if ctx.metadata is not None else "" - if ctx.metadata is not None and stg is not None and hasattr(stg,'index_outputs') and getattr(stg,'index_outputs'): - LOGGER.debug(f"Adding index voucher{inds} {tags=}") - splited_class_name = class_name.split('.')[-1] - add_output_index(ctx.metadata,OutputType.voucher,ctx.n_vouchers,ctx.module,splited_class_name,tags) - - LOGGER.debug(f"Sending voucher{inds}") - ctx.rollup.voucher({destination:destination,payload:bytes2hex(payload)}) - ctx.n_vouchers += 1 - -submit_contract_call = send_voucher - - -### -# Helpers - -def _make_query(func,model,has_param,module,**func_configs): - @helpers.db_session - def query(rollup: Rollup, params: URLParameters) -> bool: - try: - ctx = Context - # TODO: accept abi encode or json (for larger post requests, configured in settings) - # Decoding url parameters - param_list = [] - if has_param: - hints = get_type_hints(model) - fields = [] - values = [] - model_fields = model.__fields__.keys() - for k in model_fields: - if k in params.query_params: - field_str = str(hints[k]) - if field_str.startswith('typing.List') or field_str.startswith('typing.Optional[typing.List'): - fields.append(k) - values.append(params.query_params[k]) - else: - fields.append(k) - values.append(params.query_params[k][0]) - param_list.append(model.parse_obj(dict(zip(fields, values)))) - - extended_model = func_configs.get("extended_model") - if extended_model is not None: - extended_hints = get_type_hints(extended_model) - for k in list(set(extended_model.__fields__.keys()).difference(model_fields)): - if k in params.query_params: - field_str = str(extended_hints[k]) - if field_str.startswith('typing.List') or field_str.startswith('typing.Optional[typing.List'): - fields.append(k) - values.append(params.query_params[k]) - else: - fields.append(k) - values.append(params.query_params[k][0]) - func_configs["extended_params"] = extended_model.parse_obj(dict(zip(fields, values))) - - ctx.set_context(rollup,None,module,**func_configs) - res = func(*param_list) - except Exception as e: - msg = f"Error: {e}" - LOGGER.error(msg) - if logging.root.level <= logging.DEBUG: - traceback.print_exc() - add_output(msg) - return False - finally: - ctx.clear_context() - return res - return query - -def _make_mut(func,model,has_param,module, **kwargs): - @helpers.db_session - def mut(rollup: Rollup, data: RollupData) -> bool: - try: - ctx = Context - ctx.set_context(rollup,data.metadata,module,**kwargs) - payload = data.bytes_payload()[4:] - param_list = [] - if has_param: - param_list.append(abi.decode_to_model(data=payload, model=model)) #,packed=True) - res = func(*param_list) - except Exception as e: - msg = f"Error: {e}" - LOGGER.error(msg) - if logging.root.level <= logging.DEBUG: - traceback.print_exc() - add_output(msg,tags=['error']) - return False - finally: - ctx.clear_context() - return res - return mut - -# TODO add to indexer module and import it on manager - -class IndexerPayload(BaseModel): - tags: Optional[List[str]] - output_type: Optional[str] - msg_sender: Optional[str] - timestamp_gte: Optional[int] - timestamp_lte: Optional[int] - module: Optional[str] - input_index: Optional[int] - -class OutputIndex(BaseModel): - output_type: str - module: str - class_name: str - input_index: int - output_index: int - -class IndexerOutput(BaseModel): - data: List[OutputIndex] - -def indexer_query(payload: IndexerPayload) -> bool: - out = get_output_indexes(**payload.dict()) - - output_inds = [OutputIndex(output_type=r[0],module=r[1],class_name=r[2],input_index=r[3],output_index=r[4]) for r in out] - - add_output(IndexerOutput(data=output_inds)) - - return True - - ### # CLI diff --git a/cartesapp/output.py b/cartesapp/output.py index a74ff77..d750999 100644 --- a/cartesapp/output.py +++ b/cartesapp/output.py @@ -1,8 +1,231 @@ +from enum import Enum +import json +from pydantic import BaseModel +import logging +from Crypto.Hash import keccak +from cartesi import abi + +from .utils import str2bytes, hex2bytes, bytes2hex +from .context import Context +from .setting import Setting + +LOGGER = logging.getLogger(__name__) + +### +# Configs MAX_OUTPUT_SIZE = 1048567 # (2097152-17)/2 MAX_AGGREGATED_OUTPUT_SIZE = 4194248 # 4194248 = 4194304 (4MB) - 56 B (extra 0x and json formating) MAX_SPLITTABLE_OUTPUT_SIZE = 4194247 # Extra byte meand there's more data -# TODO add output classes +### +# Models + +class OutputType(Enum): + report = 0 + notice = 1 + voucher = 2 + +class OutputFormat(Enum): + abi = 0 + packed_abi = 1 + json = 2 + +### +# Outputs + +class Output: + notices_info = {} + reports_info = {} + vouchers_info = {} + add_output_index = None + def __new__(cls): + return cls + + @classmethod + def add_report(cls, klass, **kwargs): + module_name = kwargs.get('module_name') if kwargs.get('module_name')is not None else klass.__module__.split('.')[0] + class_name = klass.__name__ + abi_types = [] # abi.get_abi_types_from_model(klass) + cls.reports_info[f"{module_name}.{class_name}"] = {"module":module_name,"class":class_name,"abi_types":abi_types,"model":klass} + + @classmethod + def add_notice(cls, klass, **kwargs): + module_name = kwargs.get('module_name') if kwargs.get('module_name')is not None else klass.__module__.split('.')[0] + class_name = klass.__name__ + abi_types = abi.get_abi_types_from_model(klass) + cls.notices_info[f"{module_name}.{class_name}"] = {"module":module_name,"class":class_name,"abi_types":abi_types,"model":klass} + + @classmethod + def add_voucher(cls, klass, **kwargs): + module_name = kwargs.get('module_name') if kwargs.get('module_name')is not None else klass.__module__.split('.')[0] + class_name = klass.__name__ + abi_types = abi.get_abi_types_from_model(klass) + cls.vouchers_info[f"{module_name}.{class_name}"] = {"module":module_name,"class":class_name,"abi_types":abi_types,"model":klass} + +def notice(**kwargs): + def decorator(klass): + Output.add_notice(klass,**kwargs) + return klass + return decorator + +def report(**kwargs): + def decorator(klass): + Output.add_report(klass,**kwargs) + return klass + return decorator + +def voucher(**kwargs): + def decorator(klass): + Output.add_voucher(klass,**kwargs) + return klass + return decorator + +def normalize_output(data,encode_format) -> [bytes, str]: + if isinstance(data, bytes): return data,'bytes' + if isinstance(data, int): data.to_bytes(32,byteorder='big'),'int' + if isinstance(data, str): + if data.startswith('0x'): return hex2bytes(data[2:]),'hex' + return str2bytes(data),'str' + class_name = f"{data.__module__.split('.')[0]}.{data.__class__.__name__}" + if isinstance(data, dict) or isinstance(data, list) or isinstance(data, tuple): + return str2bytes(json.dumps(data)),class_name + if issubclass(data.__class__,BaseModel): + if encode_format == OutputFormat.abi: return abi.encode_model(data),class_name + if encode_format == OutputFormat.packed_abi: return abi.encode_model(data,True),class_name + if encode_format == OutputFormat.json: return str2bytes(data.json(exclude_unset=True,exclude_none=True)),class_name + raise Exception("Invalid output format") + +def normalize_voucher(*kargs): + if len(kargs) == 1: + if isinstance(kargs[0], bytes): return kargs[0],'bytes' + if isinstance(kargs[0], str): return hex2bytes(kargs[0]),'hex' + if issubclass(kargs[0].__class__,BaseModel): + + args_types = abi.get_abi_types_from_model(kargs[0]) + signature = f'{kargs[0].__class__.__name__}({",".join(args_types)})' + sig_hash = keccak.new(digest_bits=256) + sig_hash.update(signature.encode('utf-8')) + + selector = sig_hash.digest()[:4] + data = abi.encode_model(kargs[0]) + return selector+data,kargs[0].__class__.__name__ + raise Exception("Invalid voucher payload") + if len(kargs) == 2: + if not isinstance(kargs[0], str): raise Exception("Invalid voucher selector") + if not issubclass(kargs[1].__class__,BaseModel): raise Exception("Invalid voucher model") + + args_types = abi.get_abi_types_from_model(kargs[1]) + signature = f'{kargs[0]}({",".join(args_types)})' + sig_hash = keccak.new(digest_bits=256) + sig_hash.update(signature.encode('utf-8')) + + selector = sig_hash.digest()[:4] + data = abi.encode_model(kargs[1]) + + return selector+data,kargs[1].__class__.__name__ + # TODO: 3 is name, classes, and data, + # too many problems: how is model stored in index? how formats are defined: str or abi annotation? + raise Exception("Invalid number of arguments") + +def send_report(payload_data, **kwargs): + ctx = Context + # only one output to allow always chunking + if ctx.n_reports > 0: raise Exception("Can't add multiple reports") + + stg = Setting.settings.get(ctx.module) + + report_format = OutputFormat[getattr(stg,'report_format')] if hasattr(stg,'report_format') else OutputFormat.json + payload,class_name = normalize_output(payload_data,report_format) + + extended_params = ctx.configs.get("extended_params") + if extended_params is not None: + if ctx.metadata is None: # inspect + part = extended_params.part + payload_len = len(payload) + if payload_len > MAX_SPLITTABLE_OUTPUT_SIZE and part is not None: + if part >= 0: + startb = MAX_SPLITTABLE_OUTPUT_SIZE*(part) + endb = MAX_SPLITTABLE_OUTPUT_SIZE*(part+1) + payload = payload[startb:endb] + if endb < payload_len: payload += b'0' + + if len(payload) > MAX_AGGREGATED_OUTPUT_SIZE: + LOGGER.warn("Payload Data exceed maximum length. Truncating") + payload = payload[:MAX_AGGREGATED_OUTPUT_SIZE] + + # Always chunk if len > MAX_OUTPUT_SIZE + # if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum report length violation") + + tags = kwargs.get('tags') + add_idx = ctx.metadata is not None and stg is not None \ + and hasattr(stg,'INDEX_OUTPUTS') and getattr(stg,'INDEX_OUTPUTS') + + sent_bytes = 0 + while sent_bytes < len(payload): + inds = f" ({ctx.metadata.input_index}, {ctx.n_reports})" if ctx.metadata is not None else "" + top_bytes = sent_bytes + MAX_OUTPUT_SIZE + if top_bytes > len(payload): + top_bytes = len(payload) + + if Output.add_output_index is not None and add_idx: + splited_class_name = class_name.split('.')[-1] + LOGGER.debug(f"Adding index report{inds} {tags=}") + Output.add_output_index(ctx.metadata,OutputType.report,ctx.n_reports,ctx.module,splited_class_name,tags) + + LOGGER.debug(f"Sending report{inds} {top_bytes - sent_bytes} bytes") + ctx.rollup.report(bytes2hex(payload[sent_bytes:top_bytes])) + ctx.n_reports += 1 + sent_bytes = top_bytes + + +def send_notice(payload_data, **kwargs): + ctx = Context + stg = Setting.settings.get(ctx.module) + + notice_format = OutputFormat[getattr(stg,'notice_format')] if hasattr(stg,'notice_format') else OutputFormat.abi + payload,class_name = normalize_output(payload_data,notice_format) + + if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum output length violation") + + tags = kwargs.get('tags') + + inds = f" ({ctx.metadata.input_index}, {ctx.n_notices})" if ctx.metadata is not None else "" + if Output.add_output_index is not None and ctx.metadata is not None and stg is not None and hasattr(stg,'INDEX_OUTPUTS') and getattr(stg,'INDEX_OUTPUTS'): + LOGGER.debug(f"Adding index notice{inds} {tags=}") + splited_class_name = class_name.split('.')[-1] + Output.add_output_index(ctx.metadata,OutputType.notice,ctx.n_notices,ctx.module,splited_class_name,tags) + + LOGGER.debug(f"Sending notice{inds} {len(payload)} bytes") + ctx.rollup.notice(bytes2hex(payload)) + ctx.n_notices += 1 + +def send_voucher(destination: str, *kargs, **kwargs): + payload,class_name = normalize_voucher(*kargs) + + if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum output length violation") + + ctx = Context + stg = Setting.settings.get(ctx.module) + tags = kwargs.get('tags') + inds = f" ({ctx.metadata.input_index}, {ctx.n_vouchers})" if ctx.metadata is not None else "" + if Output.add_output_index is not None and ctx.metadata is not None and stg is not None and hasattr(stg,'INDEX_OUTPUTS') and getattr(stg,'INDEX_OUTPUTS'): + LOGGER.debug(f"Adding index voucher{inds} {tags=}") + splited_class_name = class_name.split('.')[-1] + Output.add_output_index(ctx.metadata,OutputType.voucher,ctx.n_vouchers,ctx.module,splited_class_name,tags) + + LOGGER.debug(f"Sending voucher{inds}") + ctx.rollup.voucher({"destination":destination,"payload":bytes2hex(payload)}) + ctx.n_vouchers += 1 + + +# Aliases +output = report +event = notice +contract_call = voucher + +add_output = send_report +emit_event = send_notice +submit_contract_call = send_voucher diff --git a/cartesapp/relay/__init__.py b/cartesapp/relay/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cartesapp/relay/dapp_relay.py b/cartesapp/relay/dapp_relay.py new file mode 100644 index 0000000..a23c15b --- /dev/null +++ b/cartesapp/relay/dapp_relay.py @@ -0,0 +1,56 @@ +from pydantic import BaseModel +import logging + +from cartesi.abi import Address + +from cartesapp.input import mutation +from cartesapp.context import Context + +LOGGER = logging.getLogger(__name__) + + +# config + +DAPP_RELAY_ADDRESS = "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE" + + +dapp_addresss_template = ''' +// DApp Relay +export async function dappRelay( + client:Signer, + dappAddress:string, + options?:AdvanceInputOptions +):Promise { + if (options == undefined) options = {}; + const output = await advanceDAppRelay(client,dappAddress,options).catch( + e => { + if (String(e.message).startsWith('0x')) + throw new Error(ethers.utils.toUtf8String(e.message)); + throw new Error(e.message); + }); + return output; +} +''' + +# Inputs + +class DappRelayPayload(BaseModel): + dapp_address: Address + +@mutation( + module_name='relay', + msg_sender=DAPP_RELAY_ADDRESS, + no_header=True, + packed=True, + specialized_template=dapp_addresss_template # don't create default template +) +def dapp_relay(payload: DappRelayPayload) -> bool: + if Context.dapp_address is not None: + msg = f"DApp address already set" + LOGGER.error(msg) + # add_output(msg,tags=['error']) + return False + + Context.dapp_address = payload.dapp_address + + return True \ No newline at end of file diff --git a/cartesapp/setting.py b/cartesapp/setting.py new file mode 100644 index 0000000..f6ec76c --- /dev/null +++ b/cartesapp/setting.py @@ -0,0 +1,17 @@ + + +# Settings +class Setting: + settings = {} + def __new__(cls): + return cls + + @classmethod + def add(cls, mod): + cls.settings[mod.__name__.split('.')[0]] = mod + +# def setting(**kwargs): +# def decorator(klass): +# Setting.add(klass) +# return klass +# return decorator diff --git a/cartesapp/storage.py b/cartesapp/storage.py index 7e2011b..8789304 100644 --- a/cartesapp/storage.py +++ b/cartesapp/storage.py @@ -9,14 +9,6 @@ helpers = pony.orm -### -# Models - -class OutputType(Enum): - report = 0 - notice = 1 - voucher = 2 - ### # Storage @@ -56,177 +48,3 @@ def decorator(func): Entity = Storage.db.Entity - -### -# Indexer model and methods - -class Output(Entity): - id = helpers.PrimaryKey(int, auto=True) - output_type = helpers.Required(str) # helpers.Required(OutputType) - msg_sender = helpers.Required(str, 66, lazy=True, index=True) - block_number = helpers.Required(int, lazy=True) - timestamp = helpers.Required(int, lazy=True, index=True) - epoch_index = helpers.Required(int, lazy=True) - input_index = helpers.Required(int) - output_index = helpers.Required(int) - output_module = helpers.Required(str) - output_class = helpers.Required(str) - tags = helpers.Set("OutputTag") - -class OutputTag(Entity): - id = helpers.PrimaryKey(int, auto=True) - name = helpers.Required(str, index=True) - output = helpers.Required(Output, index=True) - - -def add_output_index(metadata,output_type,output_index,output_module,output_class,tags=None): - o = Output( - output_type = output_type.name.lower(), - output_class = output_class, - output_module = output_module, - msg_sender = metadata.msg_sender.lower(), - block_number = metadata.block_number, - timestamp = metadata.timestamp, - epoch_index = metadata.epoch_index, - input_index = metadata.input_index, - output_index = output_index - ) - if tags is not None: - for tag in tags: - t = OutputTag( - name = tag, - output = o - ) - -def get_output_indexes(**kwargs): - tags = kwargs.get('tags') - - output_query = Output.select() - - tag_query = OutputTag.select() - - if tags is not None and len(tags) > 0: - tag_query = tag_query.filter(lambda t: t.name in tags) - - if kwargs.get('module') is not None: - output_query = output_query.filter(lambda o: o.output_module == kwargs.get('module').lower()) - if kwargs.get('output_type') is not None: - output_query = output_query.filter(lambda o: o.output_type == kwargs.get('output_type').lower()) - if kwargs.get('msg_sender') is not None: - output_query = output_query.filter(lambda o: o.msg_sender == kwargs.get('msg_sender').lower()) - if kwargs.get('timestamp_gte') is not None: - output_query = output_query.filter(lambda o: o.timestamp >= kwargs.get('timestamp_gte')) - if kwargs.get('timestamp_lte') is not None: - output_query = output_query.filter(lambda o: o.timestamp <= kwargs.get('timestamp_lte')) - if kwargs.get('input_index') is not None: - output_query = output_query.filter(lambda o: o.input_index == kwargs.get('input_index')) - - if tags is not None and len(tags) > 0: - query = helpers.distinct( - [o.output_type,o.output_module,o.output_class,o.input_index,o.output_index] - for o in output_query for t in tag_query if t.output == o and helpers.count(t) == len(tags) - ) - else: - query = helpers.distinct( - [o.output_type,o.output_module,o.output_class,o.input_index,o.output_index] - for o in output_query for t in tag_query if t.output == o - ) - - - return query.fetch() - - - - - - - - -# import pony.orm -# from enum import Enum -# from typing import Optional, List - -# from cartesi.abi import String, Bytes, Int, UInt - -# # class EnumConverter(pony.orm.dbapiprovider.Converter): -# # def sql_type(self): -# # return "VARCHAR(30)" -# # def validate(converter, val, obj=None): -# # if isinstance(val, Enum): pass -# # elif isinstance(val, str): val = converter.py_type[val] -# # elif isinstance(val, int): val = converter.py_type(val) -# # else: throw(TypeError, "Attribute %r: expected type is 'Enum'. Got: %r" % (converter.attr, val)) -# # return val -# # def py2sql(converter, val): -# # print(f"py2sql{val=}") -# # return val.name -# # def sql2py(converter, val): -# # print(f"sql2py{val=}") -# # return converter.py_type[val].name -# # # sql2py = validate -# # dbval2val = sql2py -# # val2dbval = py2sql -# # def dbvals_equal(converter, x, y): -# # print(f"dbvals_equal{x=},{y=}") -# # if isinstance(x, Enum): x = x.name -# # elif isinstance(x, int): x = converter.py_type(x).name -# # if isinstance(y, Enum): y = y.name -# # elif isinstance(y, int): y = converter.py_type(y).name -# # return x == y - -# helpers = pony.orm -# pony.orm.set_sql_debug(True) -# db = pony.orm.Database() -# Entity = db.Entity - -# # helpers.Required(OutputType) -# class Output(Entity): -# id = helpers.PrimaryKey(int, auto=True) -# output_type = helpers.Required(str) # helpers.Required(OutputType) -# msg_sender = helpers.Required(str, lazy=True, index=True) -# block_number = helpers.Required(int, lazy=True) -# timestamp = helpers.Required(int, lazy=True, index=True) -# epoch_index = helpers.Required(int, lazy=True) -# input_index = helpers.Required(int) -# output_index = helpers.Required(int) -# output_class = helpers.Required(str) -# tags = helpers.Set("OutputTag") - -# class OutputTag(Entity): -# id = helpers.PrimaryKey(int, auto=True) -# name = helpers.Required(str, index=True) -# output = helpers.Required(Output) - -# db.bind(provider="sqlite", filename=":memory:") -# # db.provider.converter_classes.append((Enum, EnumConverter)) -# db.generate_mapping(create_tables=True) - - -# o = Output(output_type='report',msg_sender="0x0000",timestamp=16000,block_number=0,epoch_index=0,input_index=0,output_index=0,output_class='hex') - -# t1 = OutputTag(name='game',output=o) -# t2 = OutputTag(name='log',output=o) - -# helpers.select(o for o in Output for t in OutputTag if t.output == o and t.name in ['log'] and o.output_type == 'report')[:] - -# output_filter = lambda o: True -# prev_filter = output_filter -# output_filter = lambda o: prev_filter_filter(o) and o.timestamp <= - -# helpers.select(o for o in Output for t in OutputTag if t.output == o and t.name in ['log'] and o.output_type == 'report' and output_filter(o))[:] - -# tags = ['log'] - -# output_query = Output.select() - -# tag_query = OutputTag.select() - -# if tags is not None and len(tags) > 0: -# tag_query = tag_query.filter(lambda t: t.name in tags) - -# output_query = output_query.filter(lambda o: o.msg_sender == '0x0000') - -# q = helpers.distinct( -# [o.output_type,o.output_class,o.input_index,o.output_index] -# for o in output_query for t in tag_query if t.output == o -# ) \ No newline at end of file diff --git a/cartesapp/template_frontend_generator.py b/cartesapp/template_frontend_generator.py index 4777e44..71870f4 100644 --- a/cartesapp/template_frontend_generator.py +++ b/cartesapp/template_frontend_generator.py @@ -5,6 +5,7 @@ import tempfile from jinja2 import Template from packaging.version import Version +import re from .output import MAX_SPLITTABLE_OUTPUT_SIZE @@ -13,34 +14,79 @@ PACKAGES_JSON_FILENAME = "package.json" TSCONFIG_JSON_FILENAME = "tsconfig.json" -def convert_camel_case(s): - splitted = s.split('_') - return splitted[0] + ''.join(i.title() for i in splitted[1:]) +def convert_camel_case(s, title_first = False): + snaked = re.sub(r'(? 0: - if 'cartesapp' not in modules_processed: - module_name = 'cartesapp' - else: - module_name = modules_to_add.pop() + while len(modules) > 0: + module_name = modules.pop() modules_processed.append(module_name) module_notices_info = [i for i in notices_info.values() if i['module'] == module_name] module_reports_info = [i for i in reports_info.values() if i['module'] == module_name] - module_vouchers_info = [] # TODO: add this (also add voucher info on manager) - module_mutations_info = [i for i in mutations_info.values() if i['module'] == module_name] + module_vouchers_info = [i for i in vouchers_info.values() if i['module'] == module_name] + module_mutations_info = [i for i in mutations_info.values() if i['module'] == module_name and i['configs'].get('specialized_template') is None] module_queries_info = [i for i in queries_info.values() if i['module'] == module_name] mutations_payload_info = [dict(p) for p in set([(("abi_types",tuple(i["abi_types"])),("model",i["model"])) for i in module_mutations_info])] @@ -58,13 +104,24 @@ def render_templates(conf,settings,mutations_info,queries_info,notices_info,repo frontend_lib_path = f"{FRONTEND_PATH}/{libs_path}/{module_name}" - if len(models) > 0: - - schema = generate_json_schema(models) + filepath = f"{frontend_lib_path}/lib.ts" + specialized_templates = '' + for i in mutations_info.values(): + if i['module'] == module_name and i['configs'].get('specialized_template'): + specialized_templates += i['configs'].get('specialized_template') + + if len(models) > 0 or len(specialized_templates) > 0: if not os.path.exists(frontend_lib_path): os.makedirs(frontend_lib_path) + with open(filepath, "w") as f: + f.write(lib_template_std_imports) + + if len(models) > 0: + + schema = generate_json_schema(models) + output_filepath = f"{frontend_lib_path}/ifaces.d.ts" schema_temp = tempfile.NamedTemporaryFile() @@ -84,63 +141,36 @@ def render_templates(conf,settings,mutations_info,queries_info,notices_info,repo schema_temp.close() - has_indexer_query = False - module_setting = settings.get(module_name) - if module_setting is not None: - has_indexer_query = getattr(module_setting,'index_outputs') - - filepath = f"{frontend_lib_path}/lib.ts" - if module_name == 'cartesapp': - # helper_template_file = open('templates/cartesapp-helpers.j2','r') - # helper_template = helper_template_file.read() - # helper_template_file.close() - - indexer_query_info = None - indexer_output_info = None - if add_indexer_query and not indexer_output_info: - indexer_query_info = queries_info[f"{conf['indexer_query'].__module__.split('.')[0]}.{conf['indexer_query'].__name__}"] - indexer_output_info = reports_info[f"{conf['indexer_output'].__module__.split('.')[0]}.{conf['indexer_output'].__name__}"] - - has_ifaces = add_indexer_query - helper_template_output = Template(helper_template).render({ - "convert_camel_case":convert_camel_case, - "add_indexer_query": add_indexer_query, - "has_ifaces": has_ifaces, - "indexer_query_info": indexer_query_info, - "indexer_output_info": indexer_output_info, - "MAX_SPLITTABLE_OUTPUT_SIZE":MAX_SPLITTABLE_OUTPUT_SIZE - }) + if len(specialized_templates) > 0: + with open(filepath, "a") as f: + f.write(specialized_templates) - with open(filepath, "w") as f: - f.write(helper_template_output) - else: - imports_template_output = Template(lib_imports).render({ + if len(models) > 0: + has_indexer_query = False + module_setting = settings.get(module_name) + if module_setting is not None and hasattr(module_setting,'INDEX_OUTPUTS'): + has_indexer_query = getattr(module_setting,'INDEX_OUTPUTS') + + # lib_template_file = open('templates/lib.j2','r') + # lib_template = lib_template_file.read() + # lib_template_file.close() + + lib_template_output = Template(lib_template).render({ + "MAX_SPLITTABLE_OUTPUT_SIZE":MAX_SPLITTABLE_OUTPUT_SIZE, + "mutations_info":module_mutations_info, + "queries_info":module_queries_info, + "mutations_payload_info":mutations_payload_info, + "queries_payload_info":queries_payload_info, + "notices_info":module_notices_info, + "reports_info":module_reports_info, + "vouchers_info":module_vouchers_info, "has_indexer_query": has_indexer_query, - "MAX_SPLITTABLE_OUTPUT_SIZE":MAX_SPLITTABLE_OUTPUT_SIZE + "list":list, + "convert_camel_case":convert_camel_case }) - with open(filepath, "w") as f: - f.write(imports_template_output) - - # lib_template_file = open('templates/lib.j2','r') - # lib_template = lib_template_file.read() - # lib_template_file.close() - - lib_template_output = Template(lib_template).render({ - "mutations_info":module_mutations_info, - "queries_info":module_queries_info, - "mutations_payload_info":mutations_payload_info, - "queries_payload_info":queries_payload_info, - "notices_info":module_notices_info, - "reports_info":module_reports_info, - "vouchers_info":module_vouchers_info, - "has_indexer_query": has_indexer_query, - "list":list, - "convert_camel_case":convert_camel_case - }) - - with open(filepath, "a") as f: - f.write(lib_template_output) + with open(filepath, "a") as f: + f.write(lib_template_output) def get_newer_version(pkg_name,req_version,orig_version): if orig_version is None: return req_version @@ -246,14 +276,14 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): # "strict": True, # "noEmitOnError": True, # # "suppressImplicitAnyIndexErrors": true, - # "target": "ES5", + "target": "es2015", # "plugins": [ # { "transform": "ts-transformer-keys/transformer" } # ] } } -helper_template = '''/* eslint-disable */ +cartesapp_utils_template = '''/* eslint-disable */ /** * This file was automatically generated by cartesapp.template_generator. * DO NOT MODIFY IT BY HAND. Instead, run the generator, @@ -270,11 +300,6 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): queryNotice, queryReport, queryVoucher, GraphqlOptions } from "cartesi-client"; -{% if has_ifaces -%} -import * as ifaces from "./ifaces"; -{% endif %} - - /** * Configs */ @@ -375,7 +400,7 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): return true; } - export(): string { + export(excludeParams: string[] = []): string { let payload: string; switch(this._model.ioType) { case IOType.mutationPayload: { @@ -394,6 +419,7 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): const paramList = Array(); for (const key in inputData) { if (inputData[key] == undefined) continue; + if (excludeParams.indexOf(key) > -1) continue; if (Array.isArray(inputData[key])) { for (const element in inputData[key]) { paramList.push(`${key}=${inputData[key][element]}`); @@ -521,6 +547,12 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): ):Promise { if (options == undefined) options = {}; options.aggregate = true; + const excludeParams: string[] = []; + const matchRoute = route.matchAll(/\{(\w+)\}/g); + for (const m of matchRoute) { + route.replace(m[0],inputData[m[0]]); + excludeParams.push(m[1]); + } const payload = `${route}${inputData.export()}` return await inspectCall(payload,options); } @@ -629,6 +661,33 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): } } +''' +cartesapp_lib_template = ''' +/* eslint-disable */ +/** + * This file was automatically generated by cartesapp.template_generator. + * DO NOT MODIFY IT BY HAND. Instead, run the generator, + */ + +import { + advanceInput, inspect, + AdvanceOutput, InspectOptions, AdvanceInputOptions, GraphqlOptions, + Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher, + advanceDAppRelay, advanceERC20Deposit, advanceERC721Deposit, advanceEtherDeposit, + queryNotice, queryReport, queryVoucher +} from "cartesi-client"; + +import { + InspectReport, outputGetters +} from "../cartesapp/utils" + +{% if add_indexer_query -%} +import * as indexerIfaces from "../indexer/ifaces"; +import * as indexerLib from "../indexer/lib" +{% endif %} + + + {% if add_indexer_query -%} interface OutMap { @@ -658,7 +717,7 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): for (const notice of advanceResult.notices) { outMap.notice[notice.index] = notice } for (const voucher of advanceResult.vouchers) { outMap.voucher[voucher.index] = voucher } - const indexerOutput: {{ indexer_output_info['model'].__name__ }} = await {{ convert_camel_case(indexer_query_info['method']) }}({input_index:input_index},{...options, decode:true, decodeModel:"{{ indexer_output_info['model'].__name__ }}"}) as {{ indexer_output_info['model'].__name__ }}; + const indexerOutput: indexerLib.{{ indexer_output_info['model'].__name__ }} = await indexerLib.{{ convert_camel_case(indexer_query_info['method']) }}({input_index:input_index},{...options, decode:true, decodeModel:"{{ indexer_output_info['model'].__name__ }}"}) as indexerLib.{{ indexer_output_info['model'].__name__ }}; const outList: any[] = []; for (const indOut of indexerOutput.data) { @@ -669,12 +728,12 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): // indexer export async function genericGetOutputs( - inputData: ifaces.{{ indexer_query_info['model'].__name__ }}, + inputData: indexerIfaces.{{ indexer_query_info['model'].__name__ }}, decoder: (data: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport, modelName:string) => any, options?:InspectOptions ):Promise { if (options == undefined) options = {}; - const indexerOutput: {{ indexer_output_info['model'].__name__ }} = await {{ convert_camel_case(indexer_query_info['method']) }}(inputData,{...options, decode:true, decodeModel:"{{ indexer_output_info['model'].__name__ }}"}) as {{ indexer_output_info['model'].__name__ }}; + const indexerOutput: indexerLib.{{ indexer_output_info['model'].__name__ }} = await indexerLib.{{ convert_camel_case(indexer_query_info['method']) }}(inputData,{...options, decode:true, decodeModel:"{{ indexer_output_info['model'].__name__ }}"}) as indexerLib.{{ indexer_output_info['model'].__name__ }}; const graphqlQueries: Promise[] = []; for (const outInd of indexerOutput.data) { const graphqlOptions: GraphqlOptions = {cartesiNodeUrl: options.cartesiNodeUrl, inputIndex: outInd.input_index, outputIndex: outInd.output_index}; @@ -687,30 +746,44 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): return Promise.all(graphqlQueries); } {% endif %} + ''' -lib_imports = '''/* eslint-disable */ +lib_template_std_imports = '''/* eslint-disable */ /** * This file was automatically generated by cartesapp.template_generator. * DO NOT MODIFY IT BY HAND. Instead, run the generator, */ import { ethers, Signer, ContractReceipt } from "ethers"; -import Ajv from "ajv" -import addFormats from "ajv-formats" -import { AdvanceOutput, InspectOptions, - Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher +import { + advanceInput, inspect, + AdvanceOutput, InspectOptions, AdvanceInputOptions, GraphqlOptions, + EtherDepositOptions, ERC20DepositOptions, ERC721DepositOptions, + Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher, + advanceDAppRelay, advanceERC20Deposit, advanceERC721Deposit, advanceEtherDeposit, + queryNotice, queryReport, queryVoucher } from "cartesi-client"; +''' + +lib_template = ''' +import Ajv from "ajv" +import addFormats from "ajv-formats" + import { genericAdvanceInput, genericInspect, IOType, Models, IOData, Output, Event, ContractCall, InspectReport, MutationOptions, QueryOptions, - CONVENTIONAL_TYPES, decodeToConventionalTypes{% if has_indexer_query -%}, genericGetOutputs, decodeAdvance{% endif %} -} from "../cartesapp/lib" + CONVENTIONAL_TYPES, decodeToConventionalTypes +} from "../cartesapp/utils" {% if has_indexer_query -%} -import * as cartesappIfaces from "../cartesapp/ifaces" +import { + genericGetOutputs, decodeAdvance +} from "../cartesapp/lib" + +import * as indexerIfaces from "../indexer/ifaces" {% endif -%} import * as ifaces from "./ifaces"; @@ -728,8 +801,6 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): }); const MAX_SPLITTABLE_OUTPUT_SIZE = {{ MAX_SPLITTABLE_OUTPUT_SIZE }}; -''' -lib_template = ''' /* * Mutations/Advances */ @@ -738,19 +809,19 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): export async function {{ convert_camel_case(info['method']) }}( client:Signer, dappAddress:string, - inputData: ifaces.{{ info['model'].__name__ }}, + inputData: ifaces.{{ convert_camel_case(info['model'].__name__,True) }}, options?:MutationOptions ):Promise { - const data: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(inputData); + const data: {{ convert_camel_case(info['model'].__name__,True) }} = new {{ convert_camel_case(info['model'].__name__,True) }}(inputData); {% if has_indexer_query -%} if (options?.decode) { options.sync = true; } - const result = await genericAdvanceInput(client,dappAddress,'{{ "0x"+info["selector"].to_bytes().hex() }}',data, options) + const result = await genericAdvanceInput(client,dappAddress,'{{ "0x"+info["selector"].to_bytes().hex() }}',data, options) if (options?.decode) { return decodeAdvance(result as AdvanceOutput,decodeToModel,options); } return result; {% else -%} - return genericAdvanceInput(client,dappAddress,'{{ "0x"+info["selector"].to_bytes().hex() }}',data, options); + return genericAdvanceInput(client,dappAddress,'{{ "0x"+info["selector"].to_bytes().hex() }}',data, options); {% endif -%} } @@ -761,11 +832,11 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): {% for info in queries_info -%} export async function {{ convert_camel_case(info['method']) }}( - inputData: ifaces.{{ info['model'].__name__ }}, + inputData: ifaces.{{ convert_camel_case(info['model'].__name__,True) }}, options?:QueryOptions ):Promise { const route = '{{ info["selector"] }}'; - {# return genericInspect(data,route,options); -#} + {# return genericInspect(data,route,options); -#} {% if info["configs"].get("splittable_output") -%} let part:number = 0; let hasMoreParts:boolean = false; @@ -773,8 +844,8 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): do { hasMoreParts = false; let inputDataSplittable = Object.assign({part},inputData); - const data: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(inputDataSplittable); - const partOutput: InspectReport = await genericInspect(data,route,options); + const data: {{ convert_camel_case(info['model'].__name__,True) }} = new {{ convert_camel_case(info['model'].__name__,True) }}(inputDataSplittable); + const partOutput: InspectReport = await genericInspect(data,route,options); let payloadHex = partOutput.payload.substring(2); if (payloadHex.length/2 > MAX_SPLITTABLE_OUTPUT_SIZE) { part++; @@ -784,8 +855,8 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): output.payload += payloadHex; } while (hasMoreParts) {% else -%} - const data: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(inputData); - const output: InspectReport = await genericInspect(data,route,options); + const data: {{ convert_camel_case(info['model'].__name__,True) }} = new {{ convert_camel_case(info['model'].__name__,True) }}(inputData); + const output: InspectReport = await genericInspect(data,route,options); {% endif -%} if (options?.decode) { return decodeToModel(output,options.decodeModel || "json"); } return output; @@ -798,7 +869,7 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): */ export async function getOutputs( - inputData: cartesappIfaces.IndexerPayload, + inputData: indexerIfaces.IndexerPayload, options?:InspectOptions ):Promise { return genericGetOutputs(inputData,decodeToModel,options); @@ -828,39 +899,39 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): } {% for info in mutations_payload_info -%} -export class {{ info['model'].__name__ }} extends IOData { constructor(data: ifaces.{{ info["model"].__name__ }}, validate: boolean = true) { super(models['{{ info["model"].__name__ }}'],data,validate); } } -export function exportTo{{ info['model'].__name__ }}(data: ifaces.{{ info["model"].__name__ }}): string { - const dataToExport: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(data); +export class {{ convert_camel_case(info['model'].__name__,True) }} extends IOData { constructor(data: ifaces.{{ info["model"].__name__ }}, validate: boolean = true) { super(models['{{ info["model"].__name__ }}'],data,validate); } } +export function exportTo{{ convert_camel_case(info['model'].__name__,True) }}(data: ifaces.{{ info["model"].__name__ }}): string { + const dataToExport: {{ convert_camel_case(info['model'].__name__,True) }} = new {{ convert_camel_case(info['model'].__name__,True) }}(data); return dataToExport.export(); } {% endfor -%} {% for info in queries_payload_info -%} -export class {{ info['model'].__name__ }} extends IOData { constructor(data: ifaces.{{ info["model"].__name__ }}, validate: boolean = true) { super(models['{{ info["model"].__name__ }}'],data,validate); } } -export function exportTo{{ info['model'].__name__ }}(data: ifaces.{{ info["model"].__name__ }}): string { - const dataToExport: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(data); +export class {{ convert_camel_case(info['model'].__name__,True) }} extends IOData { constructor(data: ifaces.{{ info["model"].__name__ }}, validate: boolean = true) { super(models['{{ info["model"].__name__ }}'],data,validate); } } +export function exportTo{{ convert_camel_case(info['model'].__name__,True) }}(data: ifaces.{{ info["model"].__name__ }}): string { + const dataToExport: {{ convert_camel_case(info['model'].__name__,True) }} = new {{ convert_camel_case(info['model'].__name__,True) }}(data); return dataToExport.export(); } {% endfor -%} {% for info in reports_info -%} -export class {{ info['class'] }} extends Output { constructor(output: CartesiReport | InspectReport) { super(models['{{ info["class"] }}'],output); } } -export function decodeTo{{ info['class'] }}(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): {{ info['class'] }} { - return new {{ info['class'] }}(output as CartesiReport); +export class {{ convert_camel_case(info['class'],True) }} extends Output { constructor(output: CartesiReport | InspectReport) { super(models['{{ info["class"] }}'],output); } } +export function decodeTo{{ convert_camel_case(info['class'],True) }}(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): {{ convert_camel_case(info['class'],True) }} { + return new {{ convert_camel_case(info['class'],True) }}(output as CartesiReport); } {% endfor -%} {% for info in notices_info -%} -export class {{ info['class'] }} extends Event { constructor(output: CartesiNotice) { super(models['{{ info["class"] }}'],output); } } -export function decodeTo{{ info['class'] }}(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): {{ info['class'] }} { - return new {{ info['class'] }}(output as CartesiNotice); +export class {{ convert_camel_case(info['class'],True) }} extends Event { constructor(output: CartesiNotice) { super(models['{{ info["class"] }}'],output); } } +export function decodeTo{{ convert_camel_case(info['class'],True) }}(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): {{ convert_camel_case(info['class'],True) }} { + return new {{ convert_camel_case(info['class'],True) }}(output as CartesiNotice); } {% endfor -%} {% for info in vouchers_info -%} -export class {{ info['class'] }} extends ConrtacCall { constructor(output: CartesiVoucher) { super(models['{{ info["class"] }}'],output); } } -export function decodeTo{{ info['class'] }}(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): {{ info['class'] }} { - return new {{ info['class'] }}(output as CartesiVoucher); +export class {{ convert_camel_case(info['class'],True) }} extends ContractCall { constructor(output: CartesiVoucher) { super(models['{{ info["class"] }}'],output); } } +export function decodeTo{{ convert_camel_case(info['class'],True) }}(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): {{ convert_camel_case(info['class'],True) }} { + return new {{ convert_camel_case(info['class'],True) }}(output as CartesiVoucher); } {% endfor %} @@ -892,8 +963,8 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): ioType:IOType.report, abiTypes:{{ info['abi_types'] }}, params:{{ list(info["model"].__fields__.keys()) }}, - decoder: decodeTo{{ info['class'] }}, - validator: ajv.compile(JSON.parse('{{ info["model"].schema_json() }}')) + decoder: decodeTo{{ convert_camel_case(info['class'],True) }}, + validator: ajv.compile(JSON.parse('{{ info["model"].schema_json() }}')) }, {% endfor -%} {% for info in notices_info -%} @@ -901,8 +972,8 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): ioType:IOType.notice, abiTypes:{{ info['abi_types'] }}, params:{{ list(info["model"].__fields__.keys()) }}, - decoder: decodeTo{{ info['class'] }}, - validator: ajv.compile(JSON.parse('{{ info["model"].schema_json() }}'.replaceAll('integer','string","format":"biginteger'))) + decoder: decodeTo{{ convert_camel_case(info['class'],True) }}, + validator: ajv.compile(JSON.parse('{{ info["model"].schema_json() }}'.replaceAll('integer','string","format":"biginteger'))) }, {% endfor -%} {% for info in vouchers_info -%} @@ -910,8 +981,8 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): ioType:IOType.voucher, abiTypes:{{ info['abi_types'] }}, params:{{ list(info["model"].__fields__.keys()) }}, - decoder: decodeTo{{ info['class'] }}, - validator: ajv.compile(JSON.parse('{{ info["model"].schema_json() }}'.replaceAll('integer','string","format":"biginteger'))) + decoder: decodeTo{{ convert_camel_case(info['class'],True) }}, + validator: ajv.compile(JSON.parse('{{ info["model"].schema_json() }}'.replaceAll('integer','string","format":"biginteger'))) }, {% endfor -%} }; diff --git a/cartesapp/utils.py b/cartesapp/utils.py index bc4adfd..faca127 100644 --- a/cartesapp/utils.py +++ b/cartesapp/utils.py @@ -1,4 +1,7 @@ +### +# Consts +right_bit = (1 << 256) ### # Conversion Functions @@ -23,6 +26,19 @@ def str2bytes(strtxt): def str2hex(strtxt): return bytes2hex(str2bytes(strtxt)) +def int2hex256(val): + return hex((val + right_bit) % right_bit) + +def hex2562int(val): + i = int(val,16) + return i - (right_bit if i >> 255 == 1 else 0) + +def uint2hex256(val): + return hex(val) + +def hex2562uint(val): + return int(val,16) + ### # Helpers diff --git a/cartesapp/wallet/__init__.py b/cartesapp/wallet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cartesapp/wallet/dapp_wallet.py b/cartesapp/wallet/dapp_wallet.py new file mode 100644 index 0000000..de7daac --- /dev/null +++ b/cartesapp/wallet/dapp_wallet.py @@ -0,0 +1,691 @@ +from pydantic import BaseModel +from typing import Optional, List, Dict, Tuple, Annotated, get_type_hints +import logging + +from cartesi.abi import Address, UInt256, Int256, Bytes, Bool, ABIType + +from cartesapp.storage import Entity, helpers +from cartesapp.input import mutation, query +from cartesapp.output import output, add_output, event, emit_event, contract_call, submit_contract_call +from cartesapp.context import get_metadata, get_dapp_address +from cartesapp.utils import int2hex256, hex2562int, uint2hex256, hex2562uint + +LOGGER = logging.getLogger(__name__) + + +# config + +ETHER_PORTAL_ADDRESS = "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044" +ERC20_PORTAL_ADDRESS = "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB" +ERC721_PORTAL_ADDRESS = "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87" + + +ether_deposit_template = ''' +// Deposit Ether +export async function depositEther( + client:Signer, + dappAddress:string, + amount:ethers.BigNumberish, + options?:EtherDepositOptions +):Promise { + if (options == undefined) options = {}; + const output = await advanceEtherDeposit(client,dappAddress,amount,options).catch( + e => { + if (String(e.message).startsWith('0x')) + throw new Error(ethers.utils.toUtf8String(e.message)); + throw new Error(e.message); + }); + return output; +} +''' + +erc20_deposit_template = ''' +// Deposit Erc20 +export async function depositErc20( + client:Signer, + dappAddress:string, + tokenAddress:string, + amount:ethers.BigNumberish, + options?:ERC20DepositOptions +):Promise { + if (options == undefined) options = {}; + const output = await advanceERC20Deposit(client,dappAddress,tokenAddress,amount,options).catch( + e => { + if (String(e.message).startsWith('0x')) + throw new Error(ethers.utils.toUtf8String(e.message)); + throw new Error(e.message); + }); + return output; +} +''' + +erc721_deposit_template = ''' +// Deposit Erc721 +export async function depositErc721( + client:Signer, + dappAddress:string, + tokenAddress:string, + tokenId:ethers.BigNumberish, + options?:ERC721DepositOptions +):Promise { + if (options == undefined) options = {}; + const output = await advanceERC721Deposit(client,dappAddress,tokenAddress,tokenId,options).catch( + e => { + if (String(e.message).startsWith('0x')) + throw new Error(ethers.utils.toUtf8String(e.message)); + throw new Error(e.message); + }); + return output; +} +''' + +def hash_class(s): + return hash(s.name) + +setattr(ABIType, "__hash__", hash_class) +UInt256List = Annotated[List[int], ABIType('uint256[]')] +Int256List = Annotated[List[int], ABIType('int256[]')] + + +# Model + +class Wallet(Entity): + owner = helpers.PrimaryKey(str, 42) + ether = helpers.Optional("Ether") + erc20 = helpers.Set("Erc20") + erc721 = helpers.Set("Erc721") + erc1155 = helpers.Set("Erc1155") + +class Ether(Entity): + wallet = helpers.PrimaryKey("Wallet") + amount = helpers.Required(str, 66) # hex + +class Erc20(Entity): + wallet = helpers.Required("Wallet") + address = helpers.Required(str, 42) + amount = helpers.Required(str, 66) # hex + helpers.PrimaryKey(wallet,address) + +class Erc721(Entity): + wallet = helpers.Required("Wallet") + address = helpers.Required(str, 42) + ids = helpers.Set("Erc721Id") + helpers.PrimaryKey(wallet,address) + +class Erc721Id(Entity): + id = helpers.Required(str, 66) + erc721 = helpers.Required("Erc721") + helpers.PrimaryKey(id,erc721) + +class Erc1155(Entity): + wallet = helpers.Required("Wallet") + address = helpers.Required(str, 42) + ids = helpers.Set("Erc1155Id") + helpers.PrimaryKey(wallet,address) + +class Erc1155Id(Entity): + erc1155 = helpers.Required("Erc1155") + amount = helpers.Required(str, 66) # hex + + +# Helpers + +def get_wallet(user_address: str | None = None): + if user_address is None: # try to get from metadata + metadata = get_metadata() + if metadata is None: + raise Exception("Can't get wallet from metadata (empty metadata)") + user_address = metadata.msg_sender + + wallet = Wallet.select(lambda r: r.owner == user_address.lower()).first() + if wallet is None: + wallet = Wallet(owner = user_address.lower()) + return wallet + +# Inputs + +class DepositEtherPayload(BaseModel): + sender: Address + amount: UInt256 + execLayerData: Bytes + +class WithdrawEtherPayload(BaseModel): + amount: UInt256 + execLayerData: Bytes + +class TransferEtherPayload(BaseModel): + receiver: Address + amount: UInt256 + execLayerData: Bytes + +class DepositErc20Payload(BaseModel): + result: Bool + token: Address + sender: Address + amount: UInt256 + execLayerData: Bytes + +class WithdrawErc20Payload(BaseModel): + token: Address + amount: UInt256 + execLayerData: Bytes + +class TransferErc20Payload(BaseModel): + token: Address + receiver: Address + amount: UInt256 + execLayerData: Bytes + +class DepositErc721Payload(BaseModel): + token: Address + sender: Address + id: UInt256 + execLayerData: Bytes + +class WithdrawErc721Payload(BaseModel): + token: Address + id: UInt256 + execLayerData: Bytes + +class TransferErc721Payload(BaseModel): + token: Address + receiver: Address + id: UInt256 + execLayerData: Bytes + +class BalancePayload(BaseModel): + address: str + + +# Output + +@event(module_name='wallet') +class EtherEvent(BaseModel): + user: Address + mod_amount: Int256 + balance: UInt256 + +@contract_call(module_name='wallet') +class withdrawEther(BaseModel): + user: Address + amount: UInt256 + +@event(module_name='wallet') +class Erc20Event(BaseModel): + user: Address + address: Address + mod_amount: Int256 + balance: UInt256 + +@contract_call(module_name='wallet') +class withdrawErc20(BaseModel): + user: Address + amount: UInt256 + + +@event(module_name='wallet') +class Erc721Event(BaseModel): + user: Address + address: Address + mod_id: Int256 + ids: UInt256List + +@contract_call(module_name='wallet') +class withdrawErc721(BaseModel): + sender: Address + receiver: Address + id: UInt256 + + +# @event(module_name='wallet') +# class Erc1155Event(BaseModel): +# user: Address +# address: Address +# mod_ids: Int256List +# mod_amounts:Int256List +# ids: UInt256List +# balances: UInt256List + + + +### +# Mutations + +# Ether + +@mutation( + module_name='wallet', + msg_sender=ETHER_PORTAL_ADDRESS, + no_header=True, + packed=True, + specialized_template=ether_deposit_template # don't create default template +) +def deposit_ether(payload: DepositEtherPayload) -> bool: + # get wallet + wallet = get_wallet(payload.sender) + + # get/create ether wallet + if wallet.ether is None: + wallet.ether = Ether(wallet=wallet,amount='0x00') + ether_wallet = wallet.ether + + # add deposit + new_balance = payload.amount + hex2562uint(ether_wallet.amount) + ether_wallet.amount = uint2hex256(new_balance) + + # send event + asset_event = EtherEvent( + user = wallet.owner, + mod_amount = payload.amount, + balance = new_balance + ) + emit_event(asset_event,tags=["wallet","ether",wallet.owner]) + + LOGGER.debug(f"{payload.sender} deposited {payload.amount} ether (wei)") + + return True + +@mutation(module_name='wallet') +def EtherWithdraw(payload: WithdrawEtherPayload) -> bool: # camel case name to maintain other hlf standard + dapp_address = get_dapp_address() + if dapp_address is None: + raise Exception("Dapp Address is not set") + + metadata = get_metadata() + + # get wallet + wallet = get_wallet() + + # get/create ether wallet + if wallet.ether is None: + wallet.ether = Ether(wallet=wallet,amount='0x00') + + # check balance + uint_balance = hex2562uint(wallet.ether.amount) + if uint_balance < payload.amount: + raise Exception("Wallet has insufficient ether funds") + + new_balance = uint_balance - payload.amount + wallet.ether.amount = uint2hex256(new_balance) + + # submit contract call + withdrawal = withdrawEther( + user = wallet.owner, + amount = payload.amount + ) + submit_contract_call(dapp_address,withdrawal,tags=["wallet","ether","withdrawal",wallet.owner]) + + # send event + asset_event = EtherEvent( + user = wallet.owner, + mod_amount = -payload.amount, + balance = new_balance + ) + emit_event(asset_event,tags=["wallet","ether",wallet.owner]) + + LOGGER.debug(f"{metadata.msg_sender} withdrew {payload.amount} ether (wei)") + + return True + +@mutation(module_name='wallet') +def EtherTransfer(payload: TransferEtherPayload) -> bool: # camel case name to maintain other hlf standard + metadata = get_metadata() + + return transfer_ether(metadata.msg_sender,payload.receiver,payload.amount) + +def transfer_ether(sender: str,receiver: str, amount: int): + # get wallet + wallet = get_wallet(sender) + + # get/create ether wallet + if wallet.ether is None: + wallet.ether = Ether(wallet=wallet,amount='0x0') + + # check balance + uint_balance = hex2562uint(wallet.ether.amount) + if uint_balance < amount: + raise Exception("Wallet has insufficient ether funds") + + new_balance = uint_balance - amount + wallet.ether.amount = uint2hex256(new_balance) + + # get receiver wallet + receiver_wallet = get_wallet(receiver) + + # get/create receiver ether wallet + if receiver_wallet.ether is None: + receiver_wallet.ether = Ether(wallet=receiver_wallet,amount='0x00') + + uint_receiver_balance = hex2562uint(receiver_wallet.ether.amount) + new_receiver_balance = uint_receiver_balance + amount + receiver_wallet.ether.amount = uint2hex256(new_receiver_balance) + + # send event + ether_easset_eventvent = EtherEvent( + user = wallet.owner, + mod_amount = -amount, + balance = new_balance + ) + emit_event(asset_event,tags=["wallet","ether",wallet.owner]) + + # send event + receiver_asset_event = EtherEvent( + user = receiver_wallet.owner, + mod_amount = amount, + balance = new_receiver_balance + ) + emit_event(receiver_asset_event,tags=["wallet","ether",receiver_wallet.owner]) + + LOGGER.debug(f"{sender} transfered {amount} ether (wei) to {receiver}") + + return True + + +# Erc20 + +@mutation( + module_name='wallet', + msg_sender=ERC20_PORTAL_ADDRESS, + no_header=True, + packed=True, + specialized_template=erc20_deposit_template # don't create default template +) +def deposit_erc20(payload: DepositErc20Payload) -> bool: + if not payload.result: + raise Exception("Erc20 deposit failed on base layer") + + # get wallet + wallet = get_wallet(payload.sender) + + # get/create erc20 wallet + erc20_wallet = Erc20.select(lambda r: r.wallet == wallet and r.address == payload.token.lower()).first() + if erc20_wallet is None: + erc20_wallet = Erc20(wallet=wallet,address=payload.token.lower(),amount='0x00') + + # add deposit + new_balance = payload.amount + hex2562uint(erc20_wallet.amount) + erc20_wallet.amount = uint2hex256(new_balance) + + # send event + asset_event = Erc20Event( + user = wallet.owner, + address = erc20_wallet.address, + mod_amount = payload.amount, + balance = new_balance + ) + emit_event(asset_event,tags=["wallet","erc20",erc20_wallet.address,wallet.owner]) + + LOGGER.debug(f"{payload.sender} deposited {payload.amount} of {erc20_wallet.address} tokens") + + return True + +@mutation(module_name='wallet') +def Erc20Withdraw(payload: WithdrawErc20Payload) -> bool: # camel case name to maintain other hlf standard + metadata = get_metadata() + + # get wallet + wallet = get_wallet() + + # get/create erc20 wallet + erc20_wallet = Erc20.select(lambda r: r.wallet == wallet and r.address == payload.token.lower()).first() + if erc20_wallet is None: + erc20_wallet = Erc20(wallet=wallet,address=payload.token.lower(),amount='0x00') + + # check balance + uint_balance = hex2562uint(erc20_wallet.amount) + if uint_balance < payload.amount: + raise Exception("Wallet has insufficient erc20 funds") + + new_balance = uint_balance - payload.amount + erc20_wallet.amount = uint2hex256(new_balance) + + # submit contract call + withdrawal = withdrawErc20( + user = wallet.owner, + amount = payload.amount + ) + submit_contract_call(erc20_wallet.address,"transfer",withdrawal,tags=["wallet","erc20","withdrawal",erc20_wallet.address,wallet.owner]) + + # send event + asset_event = Erc20Event( + user = wallet.owner, + address = erc20_wallet.address, + mod_amount = -payload.amount, + balance = new_balance + ) + emit_event(asset_event,tags=["wallet","erc20",erc20_wallet.address,wallet.owner]) + + LOGGER.debug(f"{metadata.msg_sender} withdrew {payload.amount} of {erc20_wallet.address} tokens") + + return True + + +@mutation(module_name='wallet') +def Erc20Transfer(payload: TransferErc20Payload) -> bool: # camel case name to maintain other hlf standard + metadata = get_metadata() + return transfer_erc20(payload.token,metadata.msg_sender,payload.receiver,payload.amount) + +def transfer_erc20(token: str, sender: str,receiver: str, amount: int): + # get wallet + wallet = get_wallet(sender) + + # get/create erc20 wallet + erc20_wallet = Erc20.select(lambda r: r.wallet == wallet and r.address == token.lower()).first() + if erc20_wallet is None: + erc20_wallet = Erc20(wallet=wallet,address=token.lower(),amount='0x00') + + # check balance + uint_balance = hex2562uint(erc20_wallet.amount) + if uint_balance < amount: + raise Exception("Wallet has insufficient erc20 funds") + + new_balance = uint_balance - amount + erc20_wallet.amount = uint2hex256(new_balance) + + # get receiver wallet + receiver_wallet = get_wallet(receiver) + + # get/create receiver erc20 wallet + receiver_erc20_wallet = Erc20.select(lambda r: r.wallet == receiver_wallet and r.address == token.lower()).first() + if receiver_erc20_wallet is None: + receiver_erc20_wallet = Erc20(wallet=receiver_wallet,address=token.lower(),amount='0x00') + + uint_receiver_balance = hex2562uint(receiver_erc20_wallet.amount) + new_receiver_balance = uint_receiver_balance + amount + receiver_erc20_wallet.amount = uint2hex256(new_receiver_balance) + + # send event + asset_event = Erc20Event( + user = wallet.owner, + address = erc20_wallet.address, + mod_amount = -amount, + balance = new_balance + ) + emit_event(asset_event,tags=["wallet","erc20",erc20_wallet.address,wallet.owner]) + + # send event + receiver_asset_event = Erc20Event( + user = receiver_wallet.owner, + address = receiver_erc20_wallet.address, + mod_amount = amount, + balance = new_receiver_balance + ) + emit_event(receiver_asset_event,tags=["wallet","erc20",erc20_wallet.address,receiver_wallet.owner]) + + LOGGER.debug(f"{sender} transfered {amount} of {token} to {receiver}") + + return True + + +# Erc721 + +@mutation( + module_name='wallet', + msg_sender=ERC721_PORTAL_ADDRESS, + no_header=True, + packed=True, + specialized_template=erc721_deposit_template # don't create default template +) +def deposit_erc721(payload: DepositErc721Payload) -> bool: + # get wallet + wallet = get_wallet(payload.sender) + + # get/create erc721 wallet + erc721_wallet = Erc721.select(lambda r: r.wallet == wallet and r.address == payload.token.lower()).first() + if erc721_wallet is None: + erc721_wallet = Erc721(wallet=wallet,address=payload.token.lower()) + + # add erc721 + Erc721Id(id=uint2hex256(payload.id),erc721=erc721_wallet) + + # send event + asset_event = Erc721Event( + user = wallet.owner, + address = erc721_wallet.address, + mod_id = payload.id, + ids = [hex2562uint(a.id) for a in erc721_wallet.ids] + ) + emit_event(asset_event,tags=["wallet","erc721",erc721_wallet.address,wallet.owner]) + + LOGGER.debug(f"{payload.sender} deposited id {payload.id} of {erc721_wallet.address}") + + return True + +@mutation(module_name='wallet') +def Erc721Withdraw(payload: WithdrawErc721Payload) -> bool: # camel case name to maintain other hlf standard + dapp_address = get_dapp_address() + if dapp_address is None: + raise Exception("Dapp Address is not set") + + metadata = get_metadata() + + # get wallet + wallet = get_wallet() + + # get/create erc721 wallet + erc721_wallet = Erc721.select(lambda r: r.wallet == wallet and r.address == payload.token.lower()).first() + if erc721_wallet is None: + erc721_wallet = Erc721(wallet=wallet,address=payload.token.lower()) + + # check balance + erc721_id = erc721_wallet.ids.select(lambda r: r.id == uint2hex256(payload.id)).first() + if erc721_id is None: + raise Exception("Wallet has not erc721 id") + + erc721_id.delete() + + # submit contract call + withdrawal = withdrawErc721( + sender = dapp_address, + receiver = wallet.owner, + id = payload.id + ) + submit_contract_call(erc721_wallet.address,"safeTransferFrom",withdrawal,tags=["wallet","erc721","withdrawal",erc721_wallet.address,wallet.owner]) + + # send event + asset_event = Erc721Event( + user = wallet.owner, + address = erc721_wallet.address, + mod_id = -payload.id, + ids = [hex2562uint(a.id) for a in erc721_wallet.ids] + ) + emit_event(asset_event,tags=["wallet","erc721",erc721_wallet.address,wallet.owner]) + + LOGGER.debug(f"{metadata.msg_sender} withdrew id {payload.id} of {erc721_wallet.address}") + + return True + + +@mutation(module_name='wallet') +def Erc721Transfer(payload: TransferErc721Payload) -> bool: # camel case name to maintain other hlf standard + metadata = get_metadata() + return transfer_erc721(payload.token,metadata.msg_sender,payload.receiver,payload.id) + +def transfer_erc721(token: str, sender: str,receiver: str, token_id: int): + # get wallet + wallet = get_wallet(sender) + + # get/create erc721 wallet + erc721_wallet = Erc721.select(lambda r: r.wallet == wallet and r.address == token.lower()).first() + if erc721_wallet is None: + erc721_wallet = Erc721(wallet=wallet,address=token.lower()) + + # check balance + erc721_id = erc721_wallet.ids.select(lambda r: r.id == uint2hex256(token_id)).first() + if erc721_id is None: + raise Exception("Wallet has not erc721 id") + + erc721_id.delete() + + # get receiver wallet + receiver_wallet = get_wallet(receiver) + + # get/create receiver erc721 wallet + receiver_erc721_wallet = Erc721.select(lambda r: r.wallet == receiver_wallet and r.address == token.lower()).first() + if receiver_erc721_wallet is None: + receiver_erc721_wallet = Erc721(wallet=receiver_wallet,address=token.lower()) + + # add erc721 + Erc721Id(id=uint2hex256(token_id),erc721=receiver_erc721_wallet) + + # send event + asset_event = Erc721Event( + user = wallet.owner, + address = erc721_wallet.address, + mod_id = -token_id, + ids = [hex2562uint(a.id) for a in erc721_wallet.ids] + ) + emit_event(asset_event,tags=["wallet","erc721",erc721_wallet.address,wallet.owner]) + + # send event + receiver_asset_event = Erc721Event( + user = receiver_wallet.owner, + address = receiver_erc721_wallet.address, + mod_id = token_id, + ids = [hex2562uint(a.id) for a in receiver_erc721_wallet.ids] + ) + emit_event(receiver_asset_event,tags=["wallet","erc721",erc721_wallet.address,receiver_wallet.owner]) + + LOGGER.debug(f"{sender} transfered {token_id} of {token} to {receiver}") + + return True + + + + +@output(module_name='wallet') +class WalletOutput(BaseModel): + ether: Optional[int] + erc20: Optional[Dict[str,int]] + erc721: Optional[Dict[str,List[int]]] + erc1155: Optional[Dict[str,Tuple[List[int],List[int]]]] + + +# Queries + +@query(module_name='wallet', path_params=['address']) +def balance(payload: BalancePayload) -> bool: + user_wallet = get_wallet(payload.address) + + wallet = {} + if user_wallet.ether is not None: + wallet["ether"] = hex2562uint(user_wallet.ether.amount) + if len(user_wallet.erc20) > 0: + wallet["erc20"] = {} + for asset in user_wallet.erc20: + wallet["erc20"][asset.address] = hex2562uint(asset.amount) + if len(user_wallet.erc721) > 0: + wallet["erc721"] = {} + for asset in user_wallet.erc721: + wallet["erc721"][asset.address] = [hex2562uint(a.id) for a in user_wallet.erc721.ids] + if len(user_wallet.erc1155) > 0: + wallet["erc1155"] = {} + for asset in user_wallet.erc1155: + wallet["erc1155"][asset.address] = (asset.ids,[hex2562int(a) for a in asset.amounts]) + + print("=== debug ===") + print(wallet) + add_output(WalletOutput.parse_obj(wallet)) + + return True + diff --git a/frontend/app/backend-libs/app/ifaces.d.ts b/frontend/app/backend-libs/app/ifaces.d.ts index 8114209..d7fe244 100644 --- a/frontend/app/backend-libs/app/ifaces.d.ts +++ b/frontend/app/backend-libs/app/ifaces.d.ts @@ -6,42 +6,79 @@ */ export interface _Master_ { - CreateScoreboardPayload: CreateScoreboardPayload; + ScoresOutput: ScoresOutput; + ScoreboardCreated: ScoreboardCreated; + CartridgeInfo: CartridgeInfo; + CartridgeInserted: CartridgeInserted; ScoreboardRemoved: ScoreboardRemoved; - RemoveCartridgePayload: RemoveCartridgePayload; + CartridgeRemoved: CartridgeRemoved; + EmptyClass: EmptyClass; ScoreboardsOutput: ScoreboardsOutput; ScoreboardReplayScore: ScoreboardReplayScore; - ReplayScore: ReplayScore; - EmptyClass: EmptyClass; - Replay: Replay; + RemoveCartridgePayload: RemoveCartridgePayload; + CreateScoreboardPayload: CreateScoreboardPayload; CartridgePayloadSplittable: CartridgePayloadSplittable; - ScoreboardsPayload: ScoreboardsPayload; - CartridgeRemoved: CartridgeRemoved; - CartridgesPayload: CartridgesPayload; - CartridgeInfo: CartridgeInfo; + InserCartridgePayload: InserCartridgePayload; CartridgePayload: CartridgePayload; - ScoresOutput: ScoresOutput; - ScoreboardCreated: ScoreboardCreated; - CartridgeInserted: CartridgeInserted; - CartridgesOutput: CartridgesOutput; + Replay: Replay; ScoresPayload: ScoresPayload; - InserCartridgePayload: InserCartridgePayload; + ScoreboardsPayload: ScoreboardsPayload; + CartridgesPayload: CartridgesPayload; ScoreboardReplayPayload: ScoreboardReplayPayload; + ReplayScore: ReplayScore; + CartridgesOutput: CartridgesOutput; } -export interface CreateScoreboardPayload { - cartridge_id: string; +export interface ScoresOutput { + data: ScoreInfo[]; + total: number; + page: number; +} +export interface ScoreInfo { + user_address: string; + timestamp: number; + score: number; +} +export interface ScoreboardCreated { + scoreboard_id: string; + created_by: string; + created_at: number; +} +export interface CartridgeInfo { + id: string; name: string; - args: string; - in_card: string; - score_function: string; + user_address: string; + info?: Info; + created_at: number; + cover?: string; +} +export interface Info { + name: string; + summary?: string; + description?: string; + version?: string; + status?: string; + tags: string[]; + authors?: Author[]; + url?: string; +} +export interface Author { + name: string; + link: string; +} +export interface CartridgeInserted { + cartridge_id: string; + user_address: string; + timestamp: number; } export interface ScoreboardRemoved { scoreboard_id: string; timestamp: number; } -export interface RemoveCartridgePayload { - id: string; +export interface CartridgeRemoved { + cartridge_id: string; + timestamp: number; } +export interface EmptyClass {} export interface ScoreboardsOutput { data: ScoreboardInfo[]; total: number; @@ -66,16 +103,26 @@ export interface ScoreboardReplayScore { extra_score: number; scoreboard_id: string; } -export interface ReplayScore { +export interface RemoveCartridgePayload { + id: string; +} +export interface CreateScoreboardPayload { cartridge_id: string; - user_address: string; - timestamp: number; - score: number; - score_type?: number; - extra_score?: number; - extra?: string; + name: string; + args: string; + in_card: string; + score_function: string; +} +export interface CartridgePayloadSplittable { + id: string; + part?: number; +} +export interface InserCartridgePayload { + data: string; +} +export interface CartridgePayload { + id: string; } -export interface EmptyClass {} export interface Replay { cartridge_id: string; outcard_hash: string; @@ -83,9 +130,10 @@ export interface Replay { in_card: string; log: string; } -export interface CartridgePayloadSplittable { - id: string; - part?: number; +export interface ScoresPayload { + scoreboard_id: string; + page?: number; + page_size?: number; } export interface ScoreboardsPayload { cartridge_id: string; @@ -93,76 +141,28 @@ export interface ScoreboardsPayload { page?: number; page_size?: number; } -export interface CartridgeRemoved { - cartridge_id: string; - timestamp: number; -} export interface CartridgesPayload { name?: string; tags?: string[]; page?: number; page_size?: number; } -export interface CartridgeInfo { - id: string; - name: string; - user_address: string; - info?: Info; - created_at: number; - cover?: string; -} -export interface Info { - name: string; - summary?: string; - description?: string; - version?: string; - status?: string; - tags: string[]; - authors?: Author[]; - url?: string; -} -export interface Author { - name: string; - link: string; -} -export interface CartridgePayload { - id: string; -} -export interface ScoresOutput { - data: ScoreInfo[]; - total: number; - page: number; -} -export interface ScoreInfo { - user_address: string; - timestamp: number; - score: number; -} -export interface ScoreboardCreated { +export interface ScoreboardReplayPayload { scoreboard_id: string; - created_by: string; - created_at: number; + outcard_hash: string; + log: string; } -export interface CartridgeInserted { +export interface ReplayScore { cartridge_id: string; user_address: string; timestamp: number; + score: number; + score_type?: number; + extra_score?: number; + extra?: string; } export interface CartridgesOutput { data: CartridgeInfo[]; total: number; page: number; } -export interface ScoresPayload { - scoreboard_id: string; - page?: number; - page_size?: number; -} -export interface InserCartridgePayload { - data: string; -} -export interface ScoreboardReplayPayload { - scoreboard_id: string; - outcard_hash: string; - log: string; -} diff --git a/frontend/app/backend-libs/app/lib.ts b/frontend/app/backend-libs/app/lib.ts index 57bf5a5..cbaf8aa 100644 --- a/frontend/app/backend-libs/app/lib.ts +++ b/frontend/app/backend-libs/app/lib.ts @@ -4,21 +4,32 @@ * DO NOT MODIFY IT BY HAND. Instead, run the generator, */ import { ethers, Signer, ContractReceipt } from "ethers"; -import Ajv from "ajv" -import addFormats from "ajv-formats" -import { AdvanceOutput, InspectOptions, - Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher +import { + advanceInput, inspect, + AdvanceOutput, InspectOptions, AdvanceInputOptions, GraphqlOptions, + EtherDepositOptions, ERC20DepositOptions, ERC721DepositOptions, + Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher, + advanceDAppRelay, advanceERC20Deposit, advanceERC721Deposit, advanceEtherDeposit, + queryNotice, queryReport, queryVoucher } from "cartesi-client"; + +import Ajv from "ajv" +import addFormats from "ajv-formats" + import { genericAdvanceInput, genericInspect, IOType, Models, IOData, Output, Event, ContractCall, InspectReport, MutationOptions, QueryOptions, - CONVENTIONAL_TYPES, decodeToConventionalTypes, genericGetOutputs, decodeAdvance + CONVENTIONAL_TYPES, decodeToConventionalTypes +} from "../cartesapp/utils" + +import { + genericGetOutputs, decodeAdvance } from "../cartesapp/lib" -import * as cartesappIfaces from "../cartesapp/ifaces" +import * as indexerIfaces from "../indexer/ifaces" import * as ifaces from "./ifaces"; @@ -208,7 +219,7 @@ export async function scores( */ export async function getOutputs( - inputData: cartesappIfaces.IndexerPayload, + inputData: indexerIfaces.IndexerPayload, options?:InspectOptions ):Promise { return genericGetOutputs(inputData,decodeToModel,options); @@ -237,24 +248,6 @@ export function exportToModel(data: any, modelName: string): string { return exporter(data); } -export class CreateScoreboardPayload extends IOData { constructor(data: ifaces.CreateScoreboardPayload, validate: boolean = true) { super(models['CreateScoreboardPayload'],data,validate); } } -export function exportToCreateScoreboardPayload(data: ifaces.CreateScoreboardPayload): string { - const dataToExport: CreateScoreboardPayload = new CreateScoreboardPayload(data); - return dataToExport.export(); -} - -export class ScoreboardReplayPayload extends IOData { constructor(data: ifaces.ScoreboardReplayPayload, validate: boolean = true) { super(models['ScoreboardReplayPayload'],data,validate); } } -export function exportToScoreboardReplayPayload(data: ifaces.ScoreboardReplayPayload): string { - const dataToExport: ScoreboardReplayPayload = new ScoreboardReplayPayload(data); - return dataToExport.export(); -} - -export class EmptyClass extends IOData { constructor(data: ifaces.EmptyClass, validate: boolean = true) { super(models['EmptyClass'],data,validate); } } -export function exportToEmptyClass(data: ifaces.EmptyClass): string { - const dataToExport: EmptyClass = new EmptyClass(data); - return dataToExport.export(); -} - export class Replay extends IOData { constructor(data: ifaces.Replay, validate: boolean = true) { super(models['Replay'],data,validate); } } export function exportToReplay(data: ifaces.Replay): string { const dataToExport: Replay = new Replay(data); @@ -267,15 +260,33 @@ export function exportToRemoveCartridgePayload(data: ifaces.RemoveCartridgePaylo return dataToExport.export(); } +export class ScoreboardReplayPayload extends IOData { constructor(data: ifaces.ScoreboardReplayPayload, validate: boolean = true) { super(models['ScoreboardReplayPayload'],data,validate); } } +export function exportToScoreboardReplayPayload(data: ifaces.ScoreboardReplayPayload): string { + const dataToExport: ScoreboardReplayPayload = new ScoreboardReplayPayload(data); + return dataToExport.export(); +} + export class InserCartridgePayload extends IOData { constructor(data: ifaces.InserCartridgePayload, validate: boolean = true) { super(models['InserCartridgePayload'],data,validate); } } export function exportToInserCartridgePayload(data: ifaces.InserCartridgePayload): string { const dataToExport: InserCartridgePayload = new InserCartridgePayload(data); return dataToExport.export(); } -export class CartridgePayloadSplittable extends IOData { constructor(data: ifaces.CartridgePayloadSplittable, validate: boolean = true) { super(models['CartridgePayloadSplittable'],data,validate); } } -export function exportToCartridgePayloadSplittable(data: ifaces.CartridgePayloadSplittable): string { - const dataToExport: CartridgePayloadSplittable = new CartridgePayloadSplittable(data); +export class CreateScoreboardPayload extends IOData { constructor(data: ifaces.CreateScoreboardPayload, validate: boolean = true) { super(models['CreateScoreboardPayload'],data,validate); } } +export function exportToCreateScoreboardPayload(data: ifaces.CreateScoreboardPayload): string { + const dataToExport: CreateScoreboardPayload = new CreateScoreboardPayload(data); + return dataToExport.export(); +} + +export class EmptyClass extends IOData { constructor(data: ifaces.EmptyClass, validate: boolean = true) { super(models['EmptyClass'],data,validate); } } +export function exportToEmptyClass(data: ifaces.EmptyClass): string { + const dataToExport: EmptyClass = new EmptyClass(data); + return dataToExport.export(); +} + +export class CartridgePayload extends IOData { constructor(data: ifaces.CartridgePayload, validate: boolean = true) { super(models['CartridgePayload'],data,validate); } } +export function exportToCartridgePayload(data: ifaces.CartridgePayload): string { + const dataToExport: CartridgePayload = new CartridgePayload(data); return dataToExport.export(); } @@ -297,9 +308,9 @@ export function exportToCartridgesPayload(data: ifaces.CartridgesPayload): strin return dataToExport.export(); } -export class CartridgePayload extends IOData { constructor(data: ifaces.CartridgePayload, validate: boolean = true) { super(models['CartridgePayload'],data,validate); } } -export function exportToCartridgePayload(data: ifaces.CartridgePayload): string { - const dataToExport: CartridgePayload = new CartridgePayload(data); +export class CartridgePayloadSplittable extends IOData { constructor(data: ifaces.CartridgePayloadSplittable, validate: boolean = true) { super(models['CartridgePayloadSplittable'],data,validate); } } +export function exportToCartridgePayloadSplittable(data: ifaces.CartridgePayloadSplittable): string { + const dataToExport: CartridgePayloadSplittable = new CartridgePayloadSplittable(data); return dataToExport.export(); } @@ -359,27 +370,6 @@ export function decodeToScoreboardReplayScore(output: CartesiReport | CartesiNot */ export const models: Models = { - 'CreateScoreboardPayload': { - ioType:IOType.mutationPayload, - abiTypes:['bytes32', 'string', 'string', 'bytes', 'string'], - params:['cartridge_id', 'name', 'args', 'in_card', 'score_function'], - exporter: exportToCreateScoreboardPayload, - validator: ajv.compile(JSON.parse('{"title": "CreateScoreboardPayload", "type": "object", "properties": {"cartridge_id": {"type": "string", "format": "binary"}, "name": {"type": "string"}, "args": {"type": "string"}, "in_card": {"type": "string", "format": "binary"}, "score_function": {"type": "string"}}, "required": ["cartridge_id", "name", "args", "in_card", "score_function"]}')) - }, - 'ScoreboardReplayPayload': { - ioType:IOType.mutationPayload, - abiTypes:['bytes32', 'bytes32', 'bytes'], - params:['scoreboard_id', 'outcard_hash', 'log'], - exporter: exportToScoreboardReplayPayload, - validator: ajv.compile(JSON.parse('{"title": "ScoreboardReplayPayload", "type": "object", "properties": {"scoreboard_id": {"type": "string", "format": "binary"}, "outcard_hash": {"type": "string", "format": "binary"}, "log": {"type": "string", "format": "binary"}}, "required": ["scoreboard_id", "outcard_hash", "log"]}')) - }, - 'EmptyClass': { - ioType:IOType.mutationPayload, - abiTypes:[], - params:[], - exporter: exportToEmptyClass, - validator: ajv.compile(JSON.parse('{"title": "EmptyClass", "type": "object", "properties": {}}')) - }, 'Replay': { ioType:IOType.mutationPayload, abiTypes:['bytes32', 'bytes32', 'string', 'bytes', 'bytes'], @@ -394,6 +384,13 @@ export const models: Models = { exporter: exportToRemoveCartridgePayload, validator: ajv.compile(JSON.parse('{"title": "RemoveCartridgePayload", "type": "object", "properties": {"id": {"type": "string", "format": "binary"}}, "required": ["id"]}')) }, + 'ScoreboardReplayPayload': { + ioType:IOType.mutationPayload, + abiTypes:['bytes32', 'bytes32', 'bytes'], + params:['scoreboard_id', 'outcard_hash', 'log'], + exporter: exportToScoreboardReplayPayload, + validator: ajv.compile(JSON.parse('{"title": "ScoreboardReplayPayload", "type": "object", "properties": {"scoreboard_id": {"type": "string", "format": "binary"}, "outcard_hash": {"type": "string", "format": "binary"}, "log": {"type": "string", "format": "binary"}}, "required": ["scoreboard_id", "outcard_hash", "log"]}')) + }, 'InserCartridgePayload': { ioType:IOType.mutationPayload, abiTypes:['bytes'], @@ -401,12 +398,26 @@ export const models: Models = { exporter: exportToInserCartridgePayload, validator: ajv.compile(JSON.parse('{"title": "InserCartridgePayload", "type": "object", "properties": {"data": {"type": "string", "format": "binary"}}, "required": ["data"]}')) }, - 'CartridgePayloadSplittable': { + 'CreateScoreboardPayload': { + ioType:IOType.mutationPayload, + abiTypes:['bytes32', 'string', 'string', 'bytes', 'string'], + params:['cartridge_id', 'name', 'args', 'in_card', 'score_function'], + exporter: exportToCreateScoreboardPayload, + validator: ajv.compile(JSON.parse('{"title": "CreateScoreboardPayload", "type": "object", "properties": {"cartridge_id": {"type": "string", "format": "binary"}, "name": {"type": "string"}, "args": {"type": "string"}, "in_card": {"type": "string", "format": "binary"}, "score_function": {"type": "string"}}, "required": ["cartridge_id", "name", "args", "in_card", "score_function"]}')) + }, + 'EmptyClass': { + ioType:IOType.mutationPayload, + abiTypes:[], + params:[], + exporter: exportToEmptyClass, + validator: ajv.compile(JSON.parse('{"title": "EmptyClass", "type": "object", "properties": {}}')) + }, + 'CartridgePayload': { ioType:IOType.queryPayload, abiTypes:[], - params:['id', 'part'], - exporter: exportToCartridgePayloadSplittable, - validator: ajv.compile(JSON.parse('{"title": "CartridgePayloadSplittable", "type": "object", "properties": {"id": {"type": "string"}, "part": {"type": "integer"}}, "required": ["id"]}')) + params:['id'], + exporter: exportToCartridgePayload, + validator: ajv.compile(JSON.parse('{"title": "CartridgePayload", "type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}')) }, 'ScoreboardsPayload': { ioType:IOType.queryPayload, @@ -429,12 +440,12 @@ export const models: Models = { exporter: exportToCartridgesPayload, validator: ajv.compile(JSON.parse('{"title": "CartridgesPayload", "type": "object", "properties": {"name": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}, "page": {"type": "integer"}, "page_size": {"type": "integer"}}}')) }, - 'CartridgePayload': { + 'CartridgePayloadSplittable': { ioType:IOType.queryPayload, abiTypes:[], - params:['id'], - exporter: exportToCartridgePayload, - validator: ajv.compile(JSON.parse('{"title": "CartridgePayload", "type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}')) + params:['id', 'part'], + exporter: exportToCartridgePayloadSplittable, + validator: ajv.compile(JSON.parse('{"title": "CartridgePayloadSplittable", "type": "object", "properties": {"id": {"type": "string"}, "part": {"type": "integer"}}, "required": ["id"]}')) }, 'CartridgeInfo': { ioType:IOType.report, diff --git a/frontend/app/backend-libs/cartesapp/lib.ts b/frontend/app/backend-libs/cartesapp/lib.ts index 6f2ccfb..074b7b8 100644 --- a/frontend/app/backend-libs/cartesapp/lib.ts +++ b/frontend/app/backend-libs/cartesapp/lib.ts @@ -1,377 +1,27 @@ + /* eslint-disable */ /** * This file was automatically generated by cartesapp.template_generator. * DO NOT MODIFY IT BY HAND. Instead, run the generator, */ -import { Signer, ethers, ContractReceipt } from "ethers"; -import Ajv, { ValidateFunction } from "ajv" -import addFormats from "ajv-formats" import { advanceInput, inspect, - AdvanceOutput, InspectOptions, AdvanceInputOptions, + AdvanceOutput, InspectOptions, AdvanceInputOptions, GraphqlOptions, Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher, - Maybe, Proof, validateNoticeFromParams, wasVoucherExecutedFromParams, executeVoucherFromParams, - queryNotice, queryReport, queryVoucher, GraphqlOptions + advanceDAppRelay, advanceERC20Deposit, advanceERC721Deposit, advanceEtherDeposit, + queryNotice, queryReport, queryVoucher } from "cartesi-client"; -import * as ifaces from "./ifaces"; - - - -/** - * Configs - */ - -const ajv = new Ajv(); -addFormats(ajv); -ajv.addFormat("biginteger", (data) => { - const dataTovalidate = data.startsWith('-') ? data.substring(1) : data; - return ethers.utils.isHexString(dataTovalidate) && dataTovalidate.length % 2 == 0; -}); -const abiCoder = new ethers.utils.AbiCoder(); -export const CONVENTIONAL_TYPES: Array = ["bytes","hex","str","int","dict","list","tuple","json"]; -const MAX_SPLITTABLE_OUTPUT_SIZE = 4194247; - - -/** - * Models - */ - -export enum IOType { - report, - notice, - voucher, - mutationPayload, - queryPayload -} - -interface ModelInterface { - ioType: IOType; - abiTypes: Array; - params: Array; - decoder?(data: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): T; - exporter?(data: T): string; - validator: ValidateFunction; -} - -export interface Models { - [key: string]: ModelInterface; -} - -export interface InspectReportInput { - index?: number; -} - -export interface InspectReport { - payload: string; - input?: InspectReportInput; - index?: number; -} - -export interface OutputGetters { - [key: string]: (o?: GraphqlOptions) => Promise|Promise|Promise; -} - -export const outputGetters: OutputGetters = { - report: queryReport, - notice: queryNotice, - voucher: queryVoucher -} - -export interface MutationOptions extends AdvanceInputOptions { - decode?: boolean; -} - -export interface QueryOptions extends InspectOptions { - decode?: boolean; - decodeModel?: string; -} - -export class IOData { - [key: string]: any; - _model: ModelInterface - - constructor(model: ModelInterface, data: T, validate: boolean = true) { - this._model = model; - for (const key of this._model.params) { - this[key] = (data as any)[key]; - } - if (validate) this.validate(); - } - - get = (): T => { - const data: any = {}; - for (const key of this._model.params) { - data[key] = this[key]; - } - return data; - } - - validate = (): boolean => { - const dataToValidate: any = { ...this.get() }; - for (const k of Object.keys(dataToValidate)) { - if (ethers.BigNumber.isBigNumber(dataToValidate[k])) - dataToValidate[k] = dataToValidate[k].toHexString(); - } - if (!this._model.validator(dataToValidate)) - throw new Error(`Data does not implement interface: ${ajv.errorsText(this._model.validator.errors)}`); - return true; - } - - export(): string { - let payload: string; - switch(this._model.ioType) { - case IOType.mutationPayload: { - // parametrize input to url - const inputData: any = this.get(); - const paramList = Array(); - for (const key of this._model.params) { - paramList.push(inputData[key]); - } - payload = abiCoder.encode(this._model.abiTypes,paramList); - break; - } - case IOType.queryPayload: { - // parametrize input to url - const inputData: T = this.get(); - const paramList = Array(); - for (const key in inputData) { - if (inputData[key] == undefined) continue; - if (Array.isArray(inputData[key])) { - for (const element in inputData[key]) { - paramList.push(`${key}=${inputData[key][element]}`); - } - } else { - paramList.push(`${key}=${inputData[key]}`); - } - } - payload = paramList.length > 0 ? `?${paramList.join('&')}` : ""; - break; - } - default: { - throw new Error(`Invalid payload type ${this._model.ioType}`); - // break; - } - } - return payload; - } -} - -export class BasicOutput extends IOData { - _payload: string - _inputIndex?: number - _outputIndex?: number - - constructor(model: ModelInterface, payload: string, inputIndex?: number, outputIndex?: number) { - super(model,genericDecodeTo(payload,model),false); - this._inputIndex = inputIndex; - this._outputIndex = outputIndex; - this._payload = payload; - } -} - -export class Output extends BasicOutput{ - constructor(model: ModelInterface, report: CartesiReport | InspectReport) { - super(model, report.payload, report.input?.index, report.index); - } -} - -export class OutputWithProof extends BasicOutput{ - _proof: Maybe | undefined - _inputIndex: number - _outputIndex: number - - constructor(model: ModelInterface, payload: string, inputIndex: number, outputIndex: number, proof: Maybe | undefined) { - super(model, payload, inputIndex, outputIndex); - this._inputIndex = inputIndex; - this._outputIndex = outputIndex; - this._proof = proof; - } -} - -export class Event extends OutputWithProof{ - constructor(model: ModelInterface, notice: CartesiNotice) { - super(model, notice.payload, notice.input.index, notice.index, notice.proof); - } - validateOnchain = async (signer: Signer, dappAddress: string): Promise => { - if (this._proof == undefined) - throw new Error("Notice has no proof"); - return await validateNoticeFromParams(signer,dappAddress,this._payload,this._proof); - } -} - -export class ContractCall extends OutputWithProof{ - _destination: string - constructor(model: ModelInterface, voucher: CartesiVoucher) { - super(model, voucher.payload, voucher.input.index, voucher.index, voucher.proof); - this._destination = voucher.destination; - } - wasExecuted = async (signer: Signer, dappAddress: string): Promise => { - return await wasVoucherExecutedFromParams(signer,dappAddress,this._inputIndex,this._outputIndex); - } - execute = async (signer: Signer, dappAddress: string): Promise => { - if (this._proof == undefined) - throw new Error("Voucher has no proof"); - return await executeVoucherFromParams(signer,dappAddress,this._destination,this._payload,this._proof); - } -} - - -/* - * Helpers - */ - -// Advance -export async function genericAdvanceInput( - client:Signer, - dappAddress:string, - selector:string, - inputData: IOData, - options?:AdvanceInputOptions -):Promise { - if (options == undefined) options = {}; - - const payloadHex = inputData.export(); - const output = await advanceInput(client,dappAddress,selector + payloadHex.replace('0x',''),options).catch( - e => { - if (String(e.message).startsWith('0x')) - throw new Error(ethers.utils.toUtf8String(e.message)); - throw new Error(e.message); - }); +import { + InspectReport, outputGetters +} from "../cartesapp/utils" - return output; -} +import * as indexerIfaces from "../indexer/ifaces"; +import * as indexerLib from "../indexer/lib" -// Inspect -export async function inspectCall( - payload:string, - options:InspectOptions -):Promise { - options.decodeTo = "no-decode"; - const inspectResult: string = await inspect(payload,options).catch( - e => { - if (String(e.message).startsWith('0x')) - throw new Error(ethers.utils.toUtf8String(e.message)); - throw new Error(e.message); - }) as string; // hex string - return {payload:inspectResult}; -} -export async function genericInspect( - inputData: IOData, - route: string, - options?:InspectOptions -):Promise { - if (options == undefined) options = {}; - options.aggregate = true; - const payload = `${route}${inputData.export()}` - return await inspectCall(payload,options); -} -// Decode -export function genericDecodeTo(data: string,model: ModelInterface): T { - let dataObj: any; - switch(model.ioType) { - /*# case mutationPayload: { - break; - } - case queryPayload: { - break; - }*/ - case IOType.report: { - const dataStr = ethers.utils.toUtf8String(data); - try { - dataObj = JSON.parse(dataStr); - } catch(e) { - throw new Error(dataStr); - } - dataObj = JSON.parse(ethers.utils.toUtf8String(data)); - if (!model.validator(dataObj)) - throw new Error(`Data does not implement interface: ${ajv.errorsText(model.validator.errors)}`); - break; - } - case IOType.notice: { - const dataValues = abiCoder.decode(model.abiTypes,data); - dataObj = {}; - let ind = 0; - for (const key of model.params) { - dataObj[key] = dataValues[ind]; - ind++; - } - const dataToValidate = { ...dataObj }; - for (const k of Object.keys(dataToValidate)) { - if (ethers.BigNumber.isBigNumber(dataToValidate[k])) - dataToValidate[k] = dataToValidate[k].toHexString(); - } - if (!model.validator(dataToValidate)) - throw new Error(`Data does not implement interface: ${ajv.errorsText(model.validator.errors)}`); - - break; - } - case IOType.voucher: { - const abiTypes: Array = ["bytes4"].concat(model.abiTypes); - const dataValues = abiCoder.decode(abiTypes,data); - dataObj = {}; - let ind = 0; - for (const key of model.params) { - if (ind == 0) continue; // skip selector - dataObj[key] = dataValues[ind-1]; - ind++; - } - const dataToValidate = { ...dataObj }; - for (const k of Object.keys(dataToValidate)) { - if (ethers.BigNumber.isBigNumber(dataToValidate[k])) - dataToValidate[k] = dataToValidate[k].toHexString(); - } - if (!model.validator(dataToValidate)) - throw new Error(`Data does not implement interface: ${ajv.errorsText(model.validator.errors)}`); - break; - } - default: { - throw new Error(`Cannot convert ${model.ioType}`); - // break; - } - } - return dataObj; -} - -export function decodeToConventionalTypes(data: string,modelName: string): any { - if (!CONVENTIONAL_TYPES.includes(modelName)) - throw new Error(`Cannot decode to ${modelName}`); - switch(modelName) { - case "bytes": { - if (typeof data == "string") { - if (ethers.utils.isHexString(data)) - return ethers.utils.arrayify(data); - else - throw new Error(`Cannot decode to bytes`); - } - return data; - } - case "hex": { - return data; - } - case "str": { - return ethers.utils.toUtf8String(data); - } - case "int": { - if (typeof data == "string") { - if (ethers.utils.isHexString(data)) - return parseInt(data, 16); - else - throw new Error(`Cannot decode to int`); - } - if (ethers.utils.isBytes(data)) - return parseInt(ethers.utils.hexlify(data), 16); - else - throw new Error(`Cannot decode to int`); - } - case "dict": case "list": case "tuple": case "json": { - return JSON.parse(ethers.utils.toUtf8String(data)); - } - } -} interface OutMap { [key: string]: CartesiReport | CartesiNotice | CartesiVoucher; @@ -400,7 +50,7 @@ export async function decodeAdvance( for (const notice of advanceResult.notices) { outMap.notice[notice.index] = notice } for (const voucher of advanceResult.vouchers) { outMap.voucher[voucher.index] = voucher } - const indexerOutput: IndexerOutput = await indexerQuery({input_index:input_index},{...options, decode:true, decodeModel:"IndexerOutput"}) as IndexerOutput; + const indexerOutput: indexerLib.IndexerOutput = await indexerLib.indexerQuery({input_index:input_index},{...options, decode:true, decodeModel:"IndexerOutput"}) as indexerLib.IndexerOutput; const outList: any[] = []; for (const indOut of indexerOutput.data) { @@ -411,12 +61,12 @@ export async function decodeAdvance( // indexer export async function genericGetOutputs( - inputData: ifaces.IndexerPayload, + inputData: indexerIfaces.IndexerPayload, decoder: (data: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport, modelName:string) => any, options?:InspectOptions ):Promise { if (options == undefined) options = {}; - const indexerOutput: IndexerOutput = await indexerQuery(inputData,{...options, decode:true, decodeModel:"IndexerOutput"}) as IndexerOutput; + const indexerOutput: indexerLib.IndexerOutput = await indexerLib.indexerQuery(inputData,{...options, decode:true, decodeModel:"IndexerOutput"}) as indexerLib.IndexerOutput; const graphqlQueries: Promise[] = []; for (const outInd of indexerOutput.data) { const graphqlOptions: GraphqlOptions = {cartesiNodeUrl: options.cartesiNodeUrl, inputIndex: outInd.input_index, outputIndex: outInd.output_index}; @@ -429,80 +79,3 @@ export async function genericGetOutputs( return Promise.all(graphqlQueries); } -/* - * Mutations/Advances - */ - - -/* - * Queries/Inspects - */ - -export async function indexerQuery( - inputData: ifaces.IndexerPayload, - options?:QueryOptions -):Promise { - const route = 'cartesapp/indexer_query'; - const data: IndexerPayload = new IndexerPayload(inputData); - const output: InspectReport = await genericInspect(data,route,options); - if (options?.decode) { return decodeToModel(output,options.decodeModel || "json"); } - return output; -} - - - - -/** - * Models Decoders/Exporters - */ - -export function decodeToModel(data: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport, modelName: string): any { - if (modelName == undefined) - throw new Error("undefined model"); - if (CONVENTIONAL_TYPES.includes(modelName)) - return decodeToConventionalTypes(data.payload,modelName); - const decoder = models[modelName].decoder; - if (decoder == undefined) - throw new Error("undefined decoder"); - return decoder(data); -} - -export function exportToModel(data: any, modelName: string): string { - const exporter = models[modelName].exporter; - if (exporter == undefined) - throw new Error("undefined exporter"); - return exporter(data); -} - -export class IndexerPayload extends IOData { constructor(data: ifaces.IndexerPayload, validate: boolean = true) { super(models['IndexerPayload'],data,validate); } } -export function exportToIndexerPayload(data: ifaces.IndexerPayload): string { - const dataToExport: IndexerPayload = new IndexerPayload(data); - return dataToExport.export(); -} - -export class IndexerOutput extends Output { constructor(output: CartesiReport | InspectReport) { super(models['IndexerOutput'],output); } } -export function decodeToIndexerOutput(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): IndexerOutput { - return new IndexerOutput(output as CartesiReport); -} - - -/** - * Model - */ - -export const models: Models = { - 'IndexerPayload': { - ioType:IOType.queryPayload, - abiTypes:[], - params:['tags', 'output_type', 'msg_sender', 'timestamp_gte', 'timestamp_lte', 'module', 'input_index'], - exporter: exportToIndexerPayload, - validator: ajv.compile(JSON.parse('{"title": "IndexerPayload", "type": "object", "properties": {"tags": {"type": "array", "items": {"type": "string"}}, "output_type": {"type": "string"}, "msg_sender": {"type": "string"}, "timestamp_gte": {"type": "integer"}, "timestamp_lte": {"type": "integer"}, "module": {"type": "string"}, "input_index": {"type": "integer"}}}')) - }, - 'IndexerOutput': { - ioType:IOType.report, - abiTypes:[], - params:['data'], - decoder: decodeToIndexerOutput, - validator: ajv.compile(JSON.parse('{"title": "IndexerOutput", "type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/definitions/OutputIndex"}}}, "required": ["data"], "definitions": {"OutputIndex": {"title": "OutputIndex", "type": "object", "properties": {"output_type": {"type": "string"}, "module": {"type": "string"}, "class_name": {"type": "string"}, "input_index": {"type": "integer"}, "output_index": {"type": "integer"}}, "required": ["output_type", "module", "class_name", "input_index", "output_index"]}}}')) - }, - }; \ No newline at end of file diff --git a/frontend/app/backend-libs/cartesapp/utils.ts b/frontend/app/backend-libs/cartesapp/utils.ts new file mode 100644 index 0000000..139884d --- /dev/null +++ b/frontend/app/backend-libs/cartesapp/utils.ts @@ -0,0 +1,377 @@ +/* eslint-disable */ +/** + * This file was automatically generated by cartesapp.template_generator. + * DO NOT MODIFY IT BY HAND. Instead, run the generator, + */ +import { Signer, ethers, ContractReceipt } from "ethers"; +import Ajv, { ValidateFunction } from "ajv" +import addFormats from "ajv-formats" + +import { + advanceInput, inspect, + AdvanceOutput, InspectOptions, AdvanceInputOptions, + Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher, + Maybe, Proof, validateNoticeFromParams, wasVoucherExecutedFromParams, executeVoucherFromParams, + queryNotice, queryReport, queryVoucher, GraphqlOptions +} from "cartesi-client"; + +/** + * Configs + */ + +const ajv = new Ajv(); +addFormats(ajv); +ajv.addFormat("biginteger", (data) => { + const dataTovalidate = data.startsWith('-') ? data.substring(1) : data; + return ethers.utils.isHexString(dataTovalidate) && dataTovalidate.length % 2 == 0; +}); +const abiCoder = new ethers.utils.AbiCoder(); +export const CONVENTIONAL_TYPES: Array = ["bytes","hex","str","int","dict","list","tuple","json"]; +const MAX_SPLITTABLE_OUTPUT_SIZE = 4194247; + + +/** + * Models + */ + +export enum IOType { + report, + notice, + voucher, + mutationPayload, + queryPayload +} + +interface ModelInterface { + ioType: IOType; + abiTypes: Array; + params: Array; + decoder?(data: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): T; + exporter?(data: T): string; + validator: ValidateFunction; +} + +export interface Models { + [key: string]: ModelInterface; +} + +export interface InspectReportInput { + index?: number; +} + +export interface InspectReport { + payload: string; + input?: InspectReportInput; + index?: number; +} + +export interface OutputGetters { + [key: string]: (o?: GraphqlOptions) => Promise|Promise|Promise; +} + +export const outputGetters: OutputGetters = { + report: queryReport, + notice: queryNotice, + voucher: queryVoucher +} + +export interface MutationOptions extends AdvanceInputOptions { + decode?: boolean; +} + +export interface QueryOptions extends InspectOptions { + decode?: boolean; + decodeModel?: string; +} + +export class IOData { + [key: string]: any; + _model: ModelInterface + + constructor(model: ModelInterface, data: T, validate: boolean = true) { + this._model = model; + for (const key of this._model.params) { + this[key] = (data as any)[key]; + } + if (validate) this.validate(); + } + + get = (): T => { + const data: any = {}; + for (const key of this._model.params) { + data[key] = this[key]; + } + return data; + } + + validate = (): boolean => { + const dataToValidate: any = { ...this.get() }; + for (const k of Object.keys(dataToValidate)) { + if (ethers.BigNumber.isBigNumber(dataToValidate[k])) + dataToValidate[k] = dataToValidate[k].toHexString(); + } + if (!this._model.validator(dataToValidate)) + throw new Error(`Data does not implement interface: ${ajv.errorsText(this._model.validator.errors)}`); + return true; + } + + export(excludeParams: string[] = []): string { + let payload: string; + switch(this._model.ioType) { + case IOType.mutationPayload: { + // parametrize input to url + const inputData: any = this.get(); + const paramList = Array(); + for (const key of this._model.params) { + paramList.push(inputData[key]); + } + payload = abiCoder.encode(this._model.abiTypes,paramList); + break; + } + case IOType.queryPayload: { + // parametrize input to url + const inputData: T = this.get(); + const paramList = Array(); + for (const key in inputData) { + if (inputData[key] == undefined) continue; + if (excludeParams.indexOf(key) > -1) continue; + if (Array.isArray(inputData[key])) { + for (const element in inputData[key]) { + paramList.push(`${key}=${inputData[key][element]}`); + } + } else { + paramList.push(`${key}=${inputData[key]}`); + } + } + payload = paramList.length > 0 ? `?${paramList.join('&')}` : ""; + break; + } + default: { + throw new Error(`Invalid payload type ${this._model.ioType}`); + // break; + } + } + return payload; + } +} + +export class BasicOutput extends IOData { + _payload: string + _inputIndex?: number + _outputIndex?: number + + constructor(model: ModelInterface, payload: string, inputIndex?: number, outputIndex?: number) { + super(model,genericDecodeTo(payload,model),false); + this._inputIndex = inputIndex; + this._outputIndex = outputIndex; + this._payload = payload; + } +} + +export class Output extends BasicOutput{ + constructor(model: ModelInterface, report: CartesiReport | InspectReport) { + super(model, report.payload, report.input?.index, report.index); + } +} + +export class OutputWithProof extends BasicOutput{ + _proof: Maybe | undefined + _inputIndex: number + _outputIndex: number + + constructor(model: ModelInterface, payload: string, inputIndex: number, outputIndex: number, proof: Maybe | undefined) { + super(model, payload, inputIndex, outputIndex); + this._inputIndex = inputIndex; + this._outputIndex = outputIndex; + this._proof = proof; + } +} + +export class Event extends OutputWithProof{ + constructor(model: ModelInterface, notice: CartesiNotice) { + super(model, notice.payload, notice.input.index, notice.index, notice.proof); + } + validateOnchain = async (signer: Signer, dappAddress: string): Promise => { + if (this._proof == undefined) + throw new Error("Notice has no proof"); + return await validateNoticeFromParams(signer,dappAddress,this._payload,this._proof); + } +} + +export class ContractCall extends OutputWithProof{ + _destination: string + constructor(model: ModelInterface, voucher: CartesiVoucher) { + super(model, voucher.payload, voucher.input.index, voucher.index, voucher.proof); + this._destination = voucher.destination; + } + wasExecuted = async (signer: Signer, dappAddress: string): Promise => { + return await wasVoucherExecutedFromParams(signer,dappAddress,this._inputIndex,this._outputIndex); + } + execute = async (signer: Signer, dappAddress: string): Promise => { + if (this._proof == undefined) + throw new Error("Voucher has no proof"); + return await executeVoucherFromParams(signer,dappAddress,this._destination,this._payload,this._proof); + } +} + + +/* + * Helpers + */ + +// Advance +export async function genericAdvanceInput( + client:Signer, + dappAddress:string, + selector:string, + inputData: IOData, + options?:AdvanceInputOptions +):Promise { + if (options == undefined) options = {}; + + const payloadHex = inputData.export(); + const output = await advanceInput(client,dappAddress,selector + payloadHex.replace('0x',''),options).catch( + e => { + if (String(e.message).startsWith('0x')) + throw new Error(ethers.utils.toUtf8String(e.message)); + throw new Error(e.message); + }); + + return output; +} + +// Inspect +export async function inspectCall( + payload:string, + options:InspectOptions +):Promise { + options.decodeTo = "no-decode"; + const inspectResult: string = await inspect(payload,options).catch( + e => { + if (String(e.message).startsWith('0x')) + throw new Error(ethers.utils.toUtf8String(e.message)); + throw new Error(e.message); + }) as string; // hex string + return {payload:inspectResult}; +} + +export async function genericInspect( + inputData: IOData, + route: string, + options?:InspectOptions +):Promise { + if (options == undefined) options = {}; + options.aggregate = true; + const excludeParams: string[] = []; + const matchRoute = route.matchAll(/\{(\w+)\}/g); + for (const m of matchRoute) { + route.replace(m[0],inputData[m[0]]); + excludeParams.push(m[1]); + } + const payload = `${route}${inputData.export()}` + return await inspectCall(payload,options); +} + +// Decode +export function genericDecodeTo(data: string,model: ModelInterface): T { + let dataObj: any; + switch(model.ioType) { + /*# case mutationPayload: { + break; + } + case queryPayload: { + break; + }*/ + case IOType.report: { + const dataStr = ethers.utils.toUtf8String(data); + try { + dataObj = JSON.parse(dataStr); + } catch(e) { + throw new Error(dataStr); + } + dataObj = JSON.parse(ethers.utils.toUtf8String(data)); + if (!model.validator(dataObj)) + throw new Error(`Data does not implement interface: ${ajv.errorsText(model.validator.errors)}`); + break; + } + case IOType.notice: { + const dataValues = abiCoder.decode(model.abiTypes,data); + dataObj = {}; + let ind = 0; + for (const key of model.params) { + dataObj[key] = dataValues[ind]; + ind++; + } + const dataToValidate = { ...dataObj }; + for (const k of Object.keys(dataToValidate)) { + if (ethers.BigNumber.isBigNumber(dataToValidate[k])) + dataToValidate[k] = dataToValidate[k].toHexString(); + } + if (!model.validator(dataToValidate)) + throw new Error(`Data does not implement interface: ${ajv.errorsText(model.validator.errors)}`); + + break; + } + case IOType.voucher: { + const abiTypes: Array = ["bytes4"].concat(model.abiTypes); + const dataValues = abiCoder.decode(abiTypes,data); + dataObj = {}; + let ind = 0; + for (const key of model.params) { + if (ind == 0) continue; // skip selector + dataObj[key] = dataValues[ind-1]; + ind++; + } + const dataToValidate = { ...dataObj }; + for (const k of Object.keys(dataToValidate)) { + if (ethers.BigNumber.isBigNumber(dataToValidate[k])) + dataToValidate[k] = dataToValidate[k].toHexString(); + } + if (!model.validator(dataToValidate)) + throw new Error(`Data does not implement interface: ${ajv.errorsText(model.validator.errors)}`); + break; + } + default: { + throw new Error(`Cannot convert ${model.ioType}`); + // break; + } + } + return dataObj; +} + +export function decodeToConventionalTypes(data: string,modelName: string): any { + if (!CONVENTIONAL_TYPES.includes(modelName)) + throw new Error(`Cannot decode to ${modelName}`); + switch(modelName) { + case "bytes": { + if (typeof data == "string") { + if (ethers.utils.isHexString(data)) + return ethers.utils.arrayify(data); + else + throw new Error(`Cannot decode to bytes`); + } + return data; + } + case "hex": { + return data; + } + case "str": { + return ethers.utils.toUtf8String(data); + } + case "int": { + if (typeof data == "string") { + if (ethers.utils.isHexString(data)) + return parseInt(data, 16); + else + throw new Error(`Cannot decode to int`); + } + if (ethers.utils.isBytes(data)) + return parseInt(ethers.utils.hexlify(data), 16); + else + throw new Error(`Cannot decode to int`); + } + case "dict": case "list": case "tuple": case "json": { + return JSON.parse(ethers.utils.toUtf8String(data)); + } + } +} diff --git a/frontend/app/backend-libs/cartesapp/ifaces.d.ts b/frontend/app/backend-libs/indexer/ifaces.d.ts similarity index 100% rename from frontend/app/backend-libs/cartesapp/ifaces.d.ts rename to frontend/app/backend-libs/indexer/ifaces.d.ts index 1078e22..d9f996c 100644 --- a/frontend/app/backend-libs/cartesapp/ifaces.d.ts +++ b/frontend/app/backend-libs/indexer/ifaces.d.ts @@ -6,8 +6,17 @@ */ export interface _Master_ { - IndexerOutput: IndexerOutput; IndexerPayload: IndexerPayload; + IndexerOutput: IndexerOutput; +} +export interface IndexerPayload { + tags?: string[]; + output_type?: string; + msg_sender?: string; + timestamp_gte?: number; + timestamp_lte?: number; + module?: string; + input_index?: number; } export interface IndexerOutput { data: OutputIndex[]; @@ -19,12 +28,3 @@ export interface OutputIndex { input_index: number; output_index: number; } -export interface IndexerPayload { - tags?: string[]; - output_type?: string; - msg_sender?: string; - timestamp_gte?: number; - timestamp_lte?: number; - module?: string; - input_index?: number; -} diff --git a/frontend/app/backend-libs/indexer/lib.ts b/frontend/app/backend-libs/indexer/lib.ts new file mode 100644 index 0000000..5b666df --- /dev/null +++ b/frontend/app/backend-libs/indexer/lib.ts @@ -0,0 +1,119 @@ +/* eslint-disable */ +/** + * This file was automatically generated by cartesapp.template_generator. + * DO NOT MODIFY IT BY HAND. Instead, run the generator, + */ +import { ethers, Signer, ContractReceipt } from "ethers"; + +import { + advanceInput, inspect, + AdvanceOutput, InspectOptions, AdvanceInputOptions, GraphqlOptions, + EtherDepositOptions, ERC20DepositOptions, ERC721DepositOptions, + Report as CartesiReport, Notice as CartesiNotice, Voucher as CartesiVoucher, + advanceDAppRelay, advanceERC20Deposit, advanceERC721Deposit, advanceEtherDeposit, + queryNotice, queryReport, queryVoucher +} from "cartesi-client"; + + +import Ajv from "ajv" +import addFormats from "ajv-formats" + +import { + genericAdvanceInput, genericInspect, IOType, Models, + IOData, Output, Event, ContractCall, InspectReport, + MutationOptions, QueryOptions, + CONVENTIONAL_TYPES, decodeToConventionalTypes +} from "../cartesapp/utils" + +import * as ifaces from "./ifaces"; + + +/** + * Configs + */ + +const ajv = new Ajv(); +addFormats(ajv); +ajv.addFormat("biginteger", (data) => { + const dataTovalidate = data.startsWith('-') ? data.substring(1) : data; + return ethers.utils.isHexString(dataTovalidate) && dataTovalidate.length % 2 == 0; +}); +const MAX_SPLITTABLE_OUTPUT_SIZE = 4194247; + +/* + * Mutations/Advances + */ + + +/* + * Queries/Inspects + */ + +export async function indexerQuery( + inputData: ifaces.IndexerPayload, + options?:QueryOptions +):Promise { + const route = 'indexer/indexer_query'; + const data: IndexerPayload = new IndexerPayload(inputData); + const output: InspectReport = await genericInspect(data,route,options); + if (options?.decode) { return decodeToModel(output,options.decodeModel || "json"); } + return output; +} + + + + +/** + * Models Decoders/Exporters + */ + +export function decodeToModel(data: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport, modelName: string): any { + if (modelName == undefined) + throw new Error("undefined model"); + if (CONVENTIONAL_TYPES.includes(modelName)) + return decodeToConventionalTypes(data.payload,modelName); + const decoder = models[modelName].decoder; + if (decoder == undefined) + throw new Error("undefined decoder"); + return decoder(data); +} + +export function exportToModel(data: any, modelName: string): string { + const exporter = models[modelName].exporter; + if (exporter == undefined) + throw new Error("undefined exporter"); + return exporter(data); +} + +export class IndexerPayload extends IOData { constructor(data: ifaces.IndexerPayload, validate: boolean = true) { super(models['IndexerPayload'],data,validate); } } +export function exportToIndexerPayload(data: ifaces.IndexerPayload): string { + const dataToExport: IndexerPayload = new IndexerPayload(data); + return dataToExport.export(); +} + +export class IndexerOutput extends Output { constructor(output: CartesiReport | InspectReport) { super(models['IndexerOutput'],output); } } +export function decodeToIndexerOutput(output: CartesiReport | CartesiNotice | CartesiVoucher | InspectReport): IndexerOutput { + return new IndexerOutput(output as CartesiReport); +} + + +/** + * Model + */ + +export const models: Models = { + 'IndexerPayload': { + ioType:IOType.queryPayload, + abiTypes:[], + params:['tags', 'output_type', 'msg_sender', 'timestamp_gte', 'timestamp_lte', 'module', 'input_index'], + exporter: exportToIndexerPayload, + validator: ajv.compile(JSON.parse('{"title": "IndexerPayload", "type": "object", "properties": {"tags": {"type": "array", "items": {"type": "string"}}, "output_type": {"type": "string"}, "msg_sender": {"type": "string"}, "timestamp_gte": {"type": "integer"}, "timestamp_lte": {"type": "integer"}, "module": {"type": "string"}, "input_index": {"type": "integer"}}}')) + }, + 'IndexerOutput': { + ioType:IOType.report, + abiTypes:[], + params:['data'], + decoder: decodeToIndexerOutput, + validator: ajv.compile(JSON.parse('{"title": "IndexerOutput", "type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/definitions/OutputIndex"}}}, "required": ["data"], "definitions": {"OutputIndex": {"title": "OutputIndex", "type": "object", "properties": {"output_type": {"type": "string"}, "module": {"type": "string"}, "class_name": {"type": "string"}, "input_index": {"type": "integer"}, "output_index": {"type": "integer"}}, "required": ["output_type", "module", "class_name", "input_index", "output_index"]}}}')) + }, + }; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index c714696..204de1d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2015", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/misc/test_commands.md b/misc/test_commands.md index 22d4cde..57dcdc5 100644 --- a/misc/test_commands.md +++ b/misc/test_commands.md @@ -88,13 +88,13 @@ curl -s "http://localhost:8080/inspect/app/scores?scoreboard_id=a7a39b72f29718e6 indexer queries ```shell -curl -s http://localhost:8080/inspect/cartesapp/indexer_query?tags=score | jq -r '.reports[0].payload' | xxd -r -p -curl -s "http://localhost:8080/inspect/cartesapp/indexer_query?tags=replay&msg_sender=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" | jq -r '.reports[0].payload' | xxd -r -p +curl -s http://localhost:8080/inspect/indexer/indexer_query?tags=score | jq -r '.reports[0].payload' | xxd -r -p +curl -s "http://localhost:8080/inspect/indexer/indexer_query?tags=replay&msg_sender=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" | jq -r '.reports[0].payload' | xxd -r -p ``` Indexer score from scoreboard ```shell -indexer_result=$(curl -s http://localhost:8080/inspect/cartesapp/indexer_query?tags=score | jq -r '.reports[0].payload' | xxd -r -p | jq) +indexer_result=$(curl -s http://localhost:8080/inspect/indexer/indexer_query?tags=score | jq -r '.reports[0].payload' | xxd -r -p | jq) curl -s -H 'Content-Type: application/json' -X POST "http://localhost:8080/graphql" -d "{ \"query\":\"query { notice(inputIndex:$(echo $indexer_result | jq -r '.[0].input_index'),noticeIndex:$(echo $indexer_result | jq -r '.[0].output_index')) { payload}}\"}}" | jq -r '.data.notice.payload' | sed -r 's/^0x//' | tr -d '\n' | xxd -c 32 ```