diff --git a/.editorconfig b/.editorconfig index b7bac97ba6..e857f5be57 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,8 +3,8 @@ charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space -insert_final_newline = false -max_line_length = 100 +insert_final_newline = true +max_line_length = 200 tab_width = 4 ij_continuation_indent_size = 8 ij_formatter_off_tag = @formatter:off @@ -13,902 +13,3 @@ ij_formatter_tags_enabled = false ij_smart_tabs = false ij_visual_guides = none ij_wrap_on_typing = false - -[*.java] -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = true -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = true -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = true -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_annotation_parameter_wrap = off -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = off -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = off -ij_java_binary_operation_sign_on_next_line = false -ij_java_binary_operation_wrap = off -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 1 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = next_line -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = off -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = true -ij_java_class_annotation_wrap = split_into_lines -ij_java_class_brace_style = next_line -ij_java_class_count_to_use_import_on_demand = 99 -ij_java_class_names_in_javadoc = 1 -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_while_brace_force = always -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = true -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = false -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = true -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = off -ij_java_extends_list_wrap = off -ij_java_field_annotation_wrap = split_into_lines -ij_java_finally_on_new_line = true -ij_java_for_brace_force = never -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = off -ij_java_generate_final_locals = false -ij_java_generate_final_parameters = false -ij_java_if_brace_force = never -ij_java_imports_layout = $android.**,$androidx.**,$com.**,$junit.**,$net.**,$org.**,$java.**,$javax.**,$*,|,android.**,|,androidx.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,| -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = false -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 2 -ij_java_keep_blank_lines_between_package_declaration_and_header = 2 -ij_java_keep_blank_lines_in_code = 2 -ij_java_keep_blank_lines_in_declarations = 2 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = true -ij_java_keep_first_column_comment = true -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = true -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_at_first_column = true -ij_java_method_annotation_wrap = split_into_lines -ij_java_method_brace_style = next_line -ij_java_method_call_chain_wrap = off -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = off -ij_java_modifier_list_wrap = false -ij_java_names_count_to_use_import_on_demand = 99 -ij_java_new_line_after_lparen_in_record_header = false -ij_java_parameter_annotation_wrap = off -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = off -ij_java_rparen_on_new_line_in_record_header = false -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = false -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = false -ij_java_ternary_operation_wrap = off -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = off -ij_java_throws_list_wrap = off -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = off -ij_java_visibility = public -ij_java_while_brace_force = never -ij_java_while_on_new_line = true -ij_java_wrap_comments = false -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false - -[*.properties] -ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = false -ij_properties_key_value_delimiter = equals -ij_properties_spaces_around_key_value_delimiter = false - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true - -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_continuation_indent_size = 4 -ij_xml_align_attributes = false -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = false -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = true -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = true - -[{*.bash,*.sh,*.zsh}] -indent_size = 2 -tab_width = 2 -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false -ij_shell_use_unix_line_separator = true - -[{*.c,*.c++,*.cc,*.cp,*.cpp,*.cu,*.cuh,*.cxx,*.h,*.h++,*.hh,*.hp,*.hpp,*.hxx,*.i,*.icc,*.ii,*.inl,*.ino,*.ipp,*.m,*.mm,*.pch,*.tcc,*.tpp}] -ij_c_add_brief_tag = false -ij_c_add_getter_prefix = true -ij_c_add_setter_prefix = true -ij_c_align_dictionary_pair_values = false -ij_c_align_group_field_declarations = false -ij_c_align_init_list_in_columns = true -ij_c_align_multiline_array_initializer_expression = true -ij_c_align_multiline_assignment = true -ij_c_align_multiline_binary_operation = true -ij_c_align_multiline_chained_methods = false -ij_c_align_multiline_for = true -ij_c_align_multiline_ternary_operation = true -ij_c_array_initializer_comma_on_next_line = false -ij_c_array_initializer_new_line_after_left_brace = false -ij_c_array_initializer_right_brace_on_new_line = false -ij_c_array_initializer_wrap = normal -ij_c_assignment_wrap = off -ij_c_binary_operation_sign_on_next_line = false -ij_c_binary_operation_wrap = normal -ij_c_blank_lines_after_class_header = 0 -ij_c_blank_lines_after_imports = 1 -ij_c_blank_lines_around_class = 1 -ij_c_blank_lines_around_field = 0 -ij_c_blank_lines_around_field_in_interface = 0 -ij_c_blank_lines_around_method = 1 -ij_c_blank_lines_around_method_in_interface = 1 -ij_c_blank_lines_around_namespace = 0 -ij_c_blank_lines_around_properties_in_declaration = 0 -ij_c_blank_lines_around_properties_in_interface = 0 -ij_c_blank_lines_before_imports = 1 -ij_c_blank_lines_before_method_body = 0 -ij_c_block_brace_placement = end_of_line -ij_c_block_brace_style = end_of_line -ij_c_block_comment_at_first_column = true -ij_c_catch_on_new_line = false -ij_c_class_brace_style = end_of_line -ij_c_class_constructor_init_list_align_multiline = true -ij_c_class_constructor_init_list_comma_on_next_line = false -ij_c_class_constructor_init_list_new_line_after_colon = never -ij_c_class_constructor_init_list_new_line_before_colon = if_long -ij_c_class_constructor_init_list_wrap = normal -ij_c_copy_is_deep = false -ij_c_create_interface_for_categories = true -ij_c_declare_generated_methods = true -ij_c_description_include_member_names = true -ij_c_discharged_short_ternary_operator = false -ij_c_do_not_add_breaks = false -ij_c_do_while_brace_force = never -ij_c_else_on_new_line = false -ij_c_enum_constants_comma_on_next_line = false -ij_c_enum_constants_wrap = on_every_item -ij_c_for_brace_force = never -ij_c_for_statement_new_line_after_left_paren = false -ij_c_for_statement_right_paren_on_new_line = false -ij_c_for_statement_wrap = off -ij_c_function_brace_placement = end_of_line -ij_c_function_call_arguments_align_multiline = true -ij_c_function_call_arguments_align_multiline_pars = false -ij_c_function_call_arguments_comma_on_next_line = false -ij_c_function_call_arguments_new_line_after_lpar = false -ij_c_function_call_arguments_new_line_before_rpar = false -ij_c_function_call_arguments_wrap = normal -ij_c_function_non_top_after_return_type_wrap = normal -ij_c_function_parameters_align_multiline = true -ij_c_function_parameters_align_multiline_pars = false -ij_c_function_parameters_comma_on_next_line = false -ij_c_function_parameters_new_line_after_lpar = false -ij_c_function_parameters_new_line_before_rpar = false -ij_c_function_parameters_wrap = normal -ij_c_function_top_after_return_type_wrap = normal -ij_c_generate_additional_eq_operators = true -ij_c_generate_additional_rel_operators = true -ij_c_generate_class_constructor = true -ij_c_generate_comparison_operators_use_std_tie = false -ij_c_generate_instance_variables_for_properties = ask -ij_c_generate_operators_as_members = true -ij_c_header_guard_style_pattern = ${PROJECT_NAME}_${FILE_NAME}_${EXT} -ij_c_if_brace_force = never -ij_c_in_line_short_ternary_operator = true -ij_c_indent_block_comment = true -ij_c_indent_c_struct_members = 4 -ij_c_indent_case_from_switch = true -ij_c_indent_class_members = 4 -ij_c_indent_directive_as_code = false -ij_c_indent_implementation_members = 0 -ij_c_indent_inside_code_block = 4 -ij_c_indent_interface_members = 0 -ij_c_indent_interface_members_except_ivars_block = false -ij_c_indent_namespace_members = 4 -ij_c_indent_preprocessor_directive = 0 -ij_c_indent_visibility_keywords = 0 -ij_c_insert_override = true -ij_c_insert_virtual_with_override = false -ij_c_introduce_auto_vars = false -ij_c_introduce_const_params = false -ij_c_introduce_const_vars = false -ij_c_introduce_generate_property = false -ij_c_introduce_generate_synthesize = true -ij_c_introduce_globals_to_header = true -ij_c_introduce_prop_to_private_category = false -ij_c_introduce_static_consts = true -ij_c_introduce_use_ns_types = false -ij_c_ivars_prefix = _ -ij_c_keep_blank_lines_before_end = 2 -ij_c_keep_blank_lines_before_right_brace = 2 -ij_c_keep_blank_lines_in_code = 2 -ij_c_keep_blank_lines_in_declarations = 2 -ij_c_keep_case_expressions_in_one_line = false -ij_c_keep_control_statement_in_one_line = true -ij_c_keep_directive_at_first_column = true -ij_c_keep_first_column_comment = true -ij_c_keep_line_breaks = true -ij_c_keep_nested_namespaces_in_one_line = false -ij_c_keep_simple_blocks_in_one_line = true -ij_c_keep_simple_methods_in_one_line = true -ij_c_keep_structures_in_one_line = false -ij_c_lambda_capture_list_align_multiline = false -ij_c_lambda_capture_list_align_multiline_bracket = false -ij_c_lambda_capture_list_comma_on_next_line = false -ij_c_lambda_capture_list_new_line_after_lbracket = false -ij_c_lambda_capture_list_new_line_before_rbracket = false -ij_c_lambda_capture_list_wrap = off -ij_c_line_comment_add_space = false -ij_c_line_comment_at_first_column = true -ij_c_method_brace_placement = end_of_line -ij_c_method_call_arguments_align_by_colons = true -ij_c_method_call_arguments_align_multiline = false -ij_c_method_call_arguments_special_dictionary_pairs_treatment = true -ij_c_method_call_arguments_wrap = off -ij_c_method_call_chain_wrap = off -ij_c_method_parameters_align_by_colons = true -ij_c_method_parameters_align_multiline = false -ij_c_method_parameters_wrap = off -ij_c_namespace_brace_placement = end_of_line -ij_c_parentheses_expression_new_line_after_left_paren = false -ij_c_parentheses_expression_right_paren_on_new_line = false -ij_c_place_assignment_sign_on_next_line = false -ij_c_property_nonatomic = true -ij_c_put_ivars_to_implementation = true -ij_c_refactor_compatibility_aliases_and_classes = true -ij_c_refactor_properties_and_ivars = true -ij_c_release_style = ivar -ij_c_retain_object_parameters_in_constructor = true -ij_c_semicolon_after_method_signature = false -ij_c_shift_operation_align_multiline = true -ij_c_shift_operation_wrap = normal -ij_c_show_non_virtual_functions = false -ij_c_space_after_colon = true -ij_c_space_after_colon_in_selector = false -ij_c_space_after_comma = true -ij_c_space_after_cup_in_blocks = false -ij_c_space_after_dictionary_literal_colon = true -ij_c_space_after_for_semicolon = true -ij_c_space_after_init_list_colon = true -ij_c_space_after_method_parameter_type_parentheses = false -ij_c_space_after_method_return_type_parentheses = false -ij_c_space_after_pointer_in_declaration = false -ij_c_space_after_quest = true -ij_c_space_after_reference_in_declaration = false -ij_c_space_after_reference_in_rvalue = false -ij_c_space_after_structures_rbrace = true -ij_c_space_after_superclass_colon = true -ij_c_space_after_type_cast = true -ij_c_space_after_visibility_sign_in_method_declaration = true -ij_c_space_before_autorelease_pool_lbrace = true -ij_c_space_before_catch_keyword = true -ij_c_space_before_catch_left_brace = true -ij_c_space_before_catch_parentheses = true -ij_c_space_before_category_parentheses = true -ij_c_space_before_chained_send_message = true -ij_c_space_before_class_left_brace = true -ij_c_space_before_colon = true -ij_c_space_before_comma = false -ij_c_space_before_dictionary_literal_colon = false -ij_c_space_before_do_left_brace = true -ij_c_space_before_else_keyword = true -ij_c_space_before_else_left_brace = true -ij_c_space_before_for_left_brace = true -ij_c_space_before_for_parentheses = true -ij_c_space_before_for_semicolon = false -ij_c_space_before_if_left_brace = true -ij_c_space_before_if_parentheses = true -ij_c_space_before_init_list = false -ij_c_space_before_init_list_colon = true -ij_c_space_before_method_call_parentheses = false -ij_c_space_before_method_left_brace = true -ij_c_space_before_method_parentheses = false -ij_c_space_before_namespace_lbrace = true -ij_c_space_before_pointer_in_declaration = true -ij_c_space_before_property_attributes_parentheses = false -ij_c_space_before_protocols_brackets = true -ij_c_space_before_quest = true -ij_c_space_before_reference_in_declaration = true -ij_c_space_before_superclass_colon = true -ij_c_space_before_switch_left_brace = true -ij_c_space_before_switch_parentheses = true -ij_c_space_before_template_call_lt = false -ij_c_space_before_template_declaration_lt = false -ij_c_space_before_try_left_brace = true -ij_c_space_before_while_keyword = true -ij_c_space_before_while_left_brace = true -ij_c_space_before_while_parentheses = true -ij_c_space_between_adjacent_brackets = false -ij_c_space_between_operator_and_punctuator = false -ij_c_space_within_empty_array_initializer_braces = false -ij_c_spaces_around_additive_operators = true -ij_c_spaces_around_assignment_operators = true -ij_c_spaces_around_bitwise_operators = true -ij_c_spaces_around_equality_operators = true -ij_c_spaces_around_lambda_arrow = true -ij_c_spaces_around_logical_operators = true -ij_c_spaces_around_multiplicative_operators = true -ij_c_spaces_around_pm_operators = false -ij_c_spaces_around_relational_operators = true -ij_c_spaces_around_shift_operators = true -ij_c_spaces_around_unary_operator = false -ij_c_spaces_within_array_initializer_braces = false -ij_c_spaces_within_braces = true -ij_c_spaces_within_brackets = false -ij_c_spaces_within_cast_parentheses = false -ij_c_spaces_within_catch_parentheses = false -ij_c_spaces_within_category_parentheses = false -ij_c_spaces_within_empty_braces = false -ij_c_spaces_within_empty_function_call_parentheses = false -ij_c_spaces_within_empty_function_declaration_parentheses = false -ij_c_spaces_within_empty_lambda_capture_list_bracket = false -ij_c_spaces_within_empty_template_call_ltgt = false -ij_c_spaces_within_empty_template_declaration_ltgt = false -ij_c_spaces_within_for_parentheses = false -ij_c_spaces_within_function_call_parentheses = false -ij_c_spaces_within_function_declaration_parentheses = false -ij_c_spaces_within_if_parentheses = false -ij_c_spaces_within_lambda_capture_list_bracket = false -ij_c_spaces_within_method_parameter_type_parentheses = false -ij_c_spaces_within_method_return_type_parentheses = false -ij_c_spaces_within_parentheses = false -ij_c_spaces_within_property_attributes_parentheses = false -ij_c_spaces_within_protocols_brackets = false -ij_c_spaces_within_send_message_brackets = false -ij_c_spaces_within_switch_parentheses = false -ij_c_spaces_within_template_call_ltgt = false -ij_c_spaces_within_template_declaration_ltgt = false -ij_c_spaces_within_template_double_gt = true -ij_c_spaces_within_while_parentheses = false -ij_c_special_else_if_treatment = true -ij_c_superclass_list_after_colon = never -ij_c_superclass_list_align_multiline = true -ij_c_superclass_list_before_colon = if_long -ij_c_superclass_list_comma_on_next_line = false -ij_c_superclass_list_wrap = on_every_item -ij_c_tag_prefix_of_block_comment = at -ij_c_tag_prefix_of_line_comment = back_slash -ij_c_template_call_arguments_align_multiline = false -ij_c_template_call_arguments_align_multiline_pars = false -ij_c_template_call_arguments_comma_on_next_line = false -ij_c_template_call_arguments_new_line_after_lt = false -ij_c_template_call_arguments_new_line_before_gt = false -ij_c_template_call_arguments_wrap = off -ij_c_template_declaration_function_body_indent = false -ij_c_template_declaration_function_wrap = split_into_lines -ij_c_template_declaration_struct_body_indent = false -ij_c_template_declaration_struct_wrap = split_into_lines -ij_c_template_parameters_align_multiline = false -ij_c_template_parameters_align_multiline_pars = false -ij_c_template_parameters_comma_on_next_line = false -ij_c_template_parameters_new_line_after_lt = false -ij_c_template_parameters_new_line_before_gt = false -ij_c_template_parameters_wrap = off -ij_c_ternary_operation_signs_on_next_line = true -ij_c_ternary_operation_wrap = normal -ij_c_type_qualifiers_placement = before -ij_c_use_modern_casts = true -ij_c_use_setters_in_constructor = true -ij_c_while_brace_force = never -ij_c_while_on_new_line = false -ij_c_wrap_property_declaration = off - -[{*.cmake,CMakeLists.txt}] -ij_cmake_align_multiline_parameters_in_calls = false -ij_cmake_force_commands_case = 2 -ij_cmake_keep_blank_lines_in_code = 2 -ij_cmake_space_before_for_parentheses = true -ij_cmake_space_before_if_parentheses = true -ij_cmake_space_before_method_call_parentheses = false -ij_cmake_space_before_method_parentheses = false -ij_cmake_space_before_while_parentheses = true -ij_cmake_spaces_within_for_parentheses = false -ij_cmake_spaces_within_if_parentheses = false -ij_cmake_spaces_within_method_call_parentheses = false -ij_cmake_spaces_within_method_parentheses = false -ij_cmake_spaces_within_while_parentheses = false - -[{*.gant,*.gradle,*.groovy,*.gy}] -ij_groovy_align_group_field_declarations = false -ij_groovy_align_multiline_array_initializer_expression = false -ij_groovy_align_multiline_assignment = false -ij_groovy_align_multiline_binary_operation = false -ij_groovy_align_multiline_chained_methods = false -ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = true -ij_groovy_align_multiline_list_or_map = true -ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = true -ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = true -ij_groovy_align_multiline_ternary_operation = false -ij_groovy_align_multiline_throws_list = false -ij_groovy_align_named_args_in_map = true -ij_groovy_align_throws_keyword = false -ij_groovy_array_initializer_new_line_after_left_brace = false -ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = off -ij_groovy_assert_statement_wrap = off -ij_groovy_assignment_wrap = off -ij_groovy_binary_operation_wrap = off -ij_groovy_blank_lines_after_class_header = 0 -ij_groovy_blank_lines_after_imports = 1 -ij_groovy_blank_lines_after_package = 1 -ij_groovy_blank_lines_around_class = 1 -ij_groovy_blank_lines_around_field = 0 -ij_groovy_blank_lines_around_field_in_interface = 0 -ij_groovy_blank_lines_around_method = 1 -ij_groovy_blank_lines_around_method_in_interface = 1 -ij_groovy_blank_lines_before_imports = 1 -ij_groovy_blank_lines_before_method_body = 0 -ij_groovy_blank_lines_before_package = 0 -ij_groovy_block_brace_style = end_of_line -ij_groovy_block_comment_at_first_column = true -ij_groovy_call_parameters_new_line_after_left_paren = false -ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = off -ij_groovy_catch_on_new_line = false -ij_groovy_class_annotation_wrap = split_into_lines -ij_groovy_class_brace_style = end_of_line -ij_groovy_class_count_to_use_import_on_demand = 5 -ij_groovy_do_while_brace_force = never -ij_groovy_else_on_new_line = false -ij_groovy_enum_constants_wrap = off -ij_groovy_extends_keyword_wrap = off -ij_groovy_extends_list_wrap = off -ij_groovy_field_annotation_wrap = split_into_lines -ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = never -ij_groovy_for_statement_new_line_after_left_paren = false -ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = off -ij_groovy_if_brace_force = never -ij_groovy_import_annotation_wrap = 2 -ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* -ij_groovy_indent_case_from_switch = true -ij_groovy_indent_label_blocks = true -ij_groovy_insert_inner_class_imports = false -ij_groovy_keep_blank_lines_before_right_brace = 2 -ij_groovy_keep_blank_lines_in_code = 2 -ij_groovy_keep_blank_lines_in_declarations = 2 -ij_groovy_keep_control_statement_in_one_line = true -ij_groovy_keep_first_column_comment = true -ij_groovy_keep_indents_on_empty_lines = false -ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = false -ij_groovy_keep_simple_blocks_in_one_line = false -ij_groovy_keep_simple_classes_in_one_line = true -ij_groovy_keep_simple_lambdas_in_one_line = true -ij_groovy_keep_simple_methods_in_one_line = true -ij_groovy_label_indent_absolute = false -ij_groovy_label_indent_size = 0 -ij_groovy_lambda_brace_style = end_of_line -ij_groovy_layout_static_imports_separately = true -ij_groovy_line_comment_add_space = false -ij_groovy_line_comment_at_first_column = true -ij_groovy_method_annotation_wrap = split_into_lines -ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = off -ij_groovy_method_parameters_new_line_after_left_paren = false -ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = off -ij_groovy_modifier_list_wrap = false -ij_groovy_names_count_to_use_import_on_demand = 3 -ij_groovy_parameter_annotation_wrap = off -ij_groovy_parentheses_expression_new_line_after_left_paren = false -ij_groovy_parentheses_expression_right_paren_on_new_line = false -ij_groovy_prefer_parameters_wrap = false -ij_groovy_resource_list_new_line_after_left_paren = false -ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = off -ij_groovy_space_after_assert_separator = true -ij_groovy_space_after_colon = true -ij_groovy_space_after_comma = true -ij_groovy_space_after_comma_in_type_arguments = true -ij_groovy_space_after_for_semicolon = true -ij_groovy_space_after_quest = true -ij_groovy_space_after_type_cast = true -ij_groovy_space_before_annotation_parameter_list = false -ij_groovy_space_before_array_initializer_left_brace = false -ij_groovy_space_before_assert_separator = false -ij_groovy_space_before_catch_keyword = true -ij_groovy_space_before_catch_left_brace = true -ij_groovy_space_before_catch_parentheses = true -ij_groovy_space_before_class_left_brace = true -ij_groovy_space_before_closure_left_brace = true -ij_groovy_space_before_colon = true -ij_groovy_space_before_comma = false -ij_groovy_space_before_do_left_brace = true -ij_groovy_space_before_else_keyword = true -ij_groovy_space_before_else_left_brace = true -ij_groovy_space_before_finally_keyword = true -ij_groovy_space_before_finally_left_brace = true -ij_groovy_space_before_for_left_brace = true -ij_groovy_space_before_for_parentheses = true -ij_groovy_space_before_for_semicolon = false -ij_groovy_space_before_if_left_brace = true -ij_groovy_space_before_if_parentheses = true -ij_groovy_space_before_method_call_parentheses = false -ij_groovy_space_before_method_left_brace = true -ij_groovy_space_before_method_parentheses = false -ij_groovy_space_before_quest = true -ij_groovy_space_before_switch_left_brace = true -ij_groovy_space_before_switch_parentheses = true -ij_groovy_space_before_synchronized_left_brace = true -ij_groovy_space_before_synchronized_parentheses = true -ij_groovy_space_before_try_left_brace = true -ij_groovy_space_before_try_parentheses = true -ij_groovy_space_before_while_keyword = true -ij_groovy_space_before_while_left_brace = true -ij_groovy_space_before_while_parentheses = true -ij_groovy_space_in_named_argument = true -ij_groovy_space_in_named_argument_before_colon = false -ij_groovy_space_within_empty_array_initializer_braces = false -ij_groovy_space_within_empty_method_call_parentheses = false -ij_groovy_spaces_around_additive_operators = true -ij_groovy_spaces_around_assignment_operators = true -ij_groovy_spaces_around_bitwise_operators = true -ij_groovy_spaces_around_equality_operators = true -ij_groovy_spaces_around_lambda_arrow = true -ij_groovy_spaces_around_logical_operators = true -ij_groovy_spaces_around_multiplicative_operators = true -ij_groovy_spaces_around_regex_operators = true -ij_groovy_spaces_around_relational_operators = true -ij_groovy_spaces_around_shift_operators = true -ij_groovy_spaces_within_annotation_parentheses = false -ij_groovy_spaces_within_array_initializer_braces = false -ij_groovy_spaces_within_braces = true -ij_groovy_spaces_within_brackets = false -ij_groovy_spaces_within_cast_parentheses = false -ij_groovy_spaces_within_catch_parentheses = false -ij_groovy_spaces_within_for_parentheses = false -ij_groovy_spaces_within_gstring_injection_braces = false -ij_groovy_spaces_within_if_parentheses = false -ij_groovy_spaces_within_list_or_map = false -ij_groovy_spaces_within_method_call_parentheses = false -ij_groovy_spaces_within_method_parentheses = false -ij_groovy_spaces_within_parentheses = false -ij_groovy_spaces_within_switch_parentheses = false -ij_groovy_spaces_within_synchronized_parentheses = false -ij_groovy_spaces_within_try_parentheses = false -ij_groovy_spaces_within_tuple_expression = false -ij_groovy_spaces_within_while_parentheses = false -ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = off -ij_groovy_throws_keyword_wrap = off -ij_groovy_throws_list_wrap = off -ij_groovy_use_flying_geese_braces = false -ij_groovy_use_fq_class_names = false -ij_groovy_use_fq_class_names_in_javadoc = true -ij_groovy_use_relative_indents = false -ij_groovy_use_single_class_imports = true -ij_groovy_variable_annotation_wrap = off -ij_groovy_while_brace_force = never -ij_groovy_while_on_new_line = false -ij_groovy_wrap_long_lines = false - -[{*.gradle.kts,*.kt,*.kts,*.main.kts}] -ij_kotlin_align_in_columns_case_branch = false -ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = false -ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = true -ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false -ij_kotlin_assignment_wrap = normal -ij_kotlin_blank_lines_after_class_header = 0 -ij_kotlin_blank_lines_around_block_when_branches = 0 -ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = true -ij_kotlin_call_parameters_right_paren_on_new_line = true -ij_kotlin_call_parameters_wrap = on_every_item -ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL -ij_kotlin_continuation_indent_for_chained_calls = false -ij_kotlin_continuation_indent_for_expression_bodies = false -ij_kotlin_continuation_indent_in_argument_lists = false -ij_kotlin_continuation_indent_in_elvis = false -ij_kotlin_continuation_indent_in_if_conditions = false -ij_kotlin_continuation_indent_in_parameter_lists = false -ij_kotlin_continuation_indent_in_supertype_lists = false -ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = split_into_lines -ij_kotlin_finally_on_new_line = false -ij_kotlin_if_rparen_on_new_line = true -ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ -ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 2 -ij_kotlin_keep_blank_lines_in_code = 2 -ij_kotlin_keep_blank_lines_in_declarations = 2 -ij_kotlin_keep_first_column_comment = true -ij_kotlin_keep_indents_on_empty_lines = false -ij_kotlin_keep_line_breaks = true -ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_comment_add_space = false -ij_kotlin_line_comment_at_first_column = true -ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = true -ij_kotlin_method_parameters_right_paren_on_new_line = true -ij_kotlin_method_parameters_wrap = on_every_item -ij_kotlin_name_count_to_use_star_import = 5 -ij_kotlin_name_count_to_use_star_import_for_members = 3 -ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** -ij_kotlin_parameter_annotation_wrap = off -ij_kotlin_space_after_comma = true -ij_kotlin_space_after_extend_colon = true -ij_kotlin_space_after_type_colon = true -ij_kotlin_space_before_catch_parentheses = true -ij_kotlin_space_before_comma = false -ij_kotlin_space_before_extend_colon = true -ij_kotlin_space_before_for_parentheses = true -ij_kotlin_space_before_if_parentheses = true -ij_kotlin_space_before_lambda_arrow = true -ij_kotlin_space_before_type_colon = false -ij_kotlin_space_before_when_parentheses = true -ij_kotlin_space_before_while_parentheses = true -ij_kotlin_spaces_around_additive_operators = true -ij_kotlin_spaces_around_assignment_operators = true -ij_kotlin_spaces_around_equality_operators = true -ij_kotlin_spaces_around_function_type_arrow = true -ij_kotlin_spaces_around_logical_operators = true -ij_kotlin_spaces_around_multiplicative_operators = true -ij_kotlin_spaces_around_range = false -ij_kotlin_spaces_around_relational_operators = true -ij_kotlin_spaces_around_unary_operator = false -ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_use_custom_formatting_for_modifiers = true -ij_kotlin_variable_annotation_wrap = off -ij_kotlin_while_on_new_line = false -ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false - -[{*.har,*.json}] -indent_size = 2 -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = true -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal -ij_html_uniform_ident = false - -[{*.markdown,*.md}] -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 - -[{*.yaml,*.yml}] -indent_size = 2 -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true diff --git a/.github/checkstyle-rules.xml b/.github/checkstyle-rules.xml new file mode 100644 index 0000000000..4234051d17 --- /dev/null +++ b/.github/checkstyle-rules.xml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/checkstyle.yml b/.github/workflows/checkstyle.yml new file mode 100644 index 0000000000..6d5b0dc734 --- /dev/null +++ b/.github/workflows/checkstyle.yml @@ -0,0 +1,23 @@ +name: Run CheckStyle +on: pull_request + +jobs: + checkstyle_job: + runs-on: ubuntu-latest + name: Checkstyle job + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run check style + uses: nikitasavinov/checkstyle-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + checkstyle_config: '.github/checkstyle-rules.xml' + reporter: 'github-pr-review' +# filter_mode: 'file' # https://github.com/nikitasavinov/checkstyle-action + fail_on_error: true + level: 'error' + tool_name: 'CheckStyle' diff --git a/.github/workflows/ci-ubuntu.yml b/.github/workflows/ci-ubuntu.yml new file mode 100644 index 0000000000..e587cd9f28 --- /dev/null +++ b/.github/workflows/ci-ubuntu.yml @@ -0,0 +1,36 @@ +name: Unit test(Ubuntu) +on: + push: + branches: + - master + pull_request: +jobs: + test: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 11 + + - name: Run unit tests + run: sh ./build.sh + + - name: Upload Test Reports Folder + uses: actions/upload-artifact@v2 + if: ${{ always() }} # IMPORTANT: Upload reports regardless of status + with: + name: ut-reports + path: app/build/reports/tests + + - name: Upload coverage reports to Codecov + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000000..4d7e11d4bb --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,36 @@ +name: Unit test(Windows) +on: + push: + branches: + - master + pull_request: +jobs: + test: + runs-on: windows-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 11 + + - name: Run unit tests + run: sh ./build.sh + + - name: Upload Test Reports Folder + uses: actions/upload-artifact@v2 + if: ${{ always() }} # IMPORTANT: Upload reports regardless of status + with: + name: ut-reports + path: app/build/reports/tests + + - name: Upload coverage reports to Codecov + run: | + curl -Os https://uploader.codecov.io/latest/windows/codecov + chmod +x codecov + ./codecov diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7040a3c6b..a4b473b76f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,44 +1,36 @@ -name: Test +name: Unit test(MacOS) on: push: branches: - master pull_request: - branches: - - master jobs: test: runs-on: macos-latest - strategy: - matrix: - api-level: [31] - target: [default] + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - - name: checkout - uses: actions/checkout@v2 - - - name: Set up JDK 11 - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 with: + distribution: zulu java-version: 11 - - name: Build the app + - name: Run unit tests run: sh ./build.sh - - name: Run e2e tests - uses: reactivecircus/android-emulator-runner@v2 + - name: Upload Test Reports Folder + uses: actions/upload-artifact@v2 + if: ${{ always() }} # IMPORTANT: Upload reports regardless of status with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: x86_64 - profile: Nexus 6 - ram-size: 4096M - sdcard-path-or-size: 4096M - script: ./e2e.sh --CI + name: ut-reports + path: app/build/reports/tests - - name: Collect E2E tests results - if: ${{ failure() }} - uses: actions/upload-artifact@v1 - with: - name: e2e-tests-results - path: output/ \ No newline at end of file + - name: Upload coverage reports to Codecov + run: | + curl -Os https://uploader.codecov.io/latest/macos/codecov + chmod +x codecov + ./codecov diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000000..71e044a55f --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,52 @@ +name: E2e Test +on: + push: + branches: + - master + pull_request: +jobs: + test: + runs-on: self-hosted + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + timeout-minutes: 20 + strategy: + matrix: + api-level: [30] + target: [default] + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 11 + architecture: arm64 + + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'npm' + - run: npm install ganache --global + + - name: Run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: arm64-v8a + profile: Nexus 6 + channel: canary + disable-animations: true + force-avd-creation: true + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: ./e2e.sh --CI + + - name: Collect tests results + if: ${{ failure() }} + uses: actions/upload-artifact@v1 + with: + name: e2e-tests-results + path: output/ diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index c9de6639a5..756a270b76 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -1,17 +1,16 @@ name: Comments Android lint warnings on pull request -on: - pull_request: - branches: - - master +on: pull_request jobs: lint: - name: Run Lint - runs-on: ubuntu-18.04 - + name: Comments lint result on PR + runs-on: macos-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - uses: actions/checkout@v3 with: - fetch-depth: 1 + fetch-depth: 0 - name: set up JDK uses: actions/setup-java@v3 with: @@ -28,4 +27,5 @@ jobs: PR_NUMBER: ${{ github.event.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ./gradlew report -PgithubPullRequestId=$PR_NUMBER -PgithubToken=$GITHUB_TOKEN \ No newline at end of file + ./gradlew report -PgithubPullRequestId=$PR_NUMBER -PgithubToken=$GITHUB_TOKEN + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fbea837c0f..40d9fa2823 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,18 +4,18 @@ on: branches: - master pull_request: - branches: - - master jobs: lint: name: Run Lint - runs-on: ubuntu-18.04 - + runs-on: macos-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - uses: actions/checkout@v3 with: - fetch-depth: 1 - - name: set up JDK + fetch-depth: 0 + - name: Set up JDK uses: actions/setup-java@v3 with: distribution: zulu @@ -24,4 +24,4 @@ jobs: - name: Run Kotlin lint run: ./gradlew :app:detekt - name: Run Android Lint - run: ./gradlew :app:lintAnalyticsDebug \ No newline at end of file + run: ./gradlew :app:lintAnalyticsDebug diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml new file mode 100644 index 0000000000..af782045db --- /dev/null +++ b/.github/workflows/stats.yml @@ -0,0 +1,13 @@ +name: Pull Request Stats +on: + pull_request: + types: [opened] +jobs: + stats: + runs-on: ubuntu-latest + steps: + - name: Run pull request stats + uses: flowwer-dev/pull-request-stats@master + with: + charts: true + sort-by: 'COMMENTS' diff --git a/.gitignore b/.gitignore index 34074fb4f6..5433bb5dab 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ app/full-r8-config.txt app/awallet app/analytics app/noAnalytics +app/CMakeCache.txt # Files for the Dalvik VM *.dex @@ -70,4 +71,8 @@ gen-external-apklibs fastlane/.google-play-key.json DCIM/ +.project vendor/ +.project +output/ +node_modules/ diff --git a/README.md b/README.md index 4b0811bb8f..c08e48c178 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ # AlphaWallet - Advanced, Open Source Ethereum Mobile Wallet & dApp Browser for Android -[![Build Status](https://api.travis-ci.com/AlphaWallet/alpha-wallet-android.svg?branch=master)](https://api.travis-ci.com/AlphaWallet/alpha-wallet-android.svg?branch=master) +[![Lint](https://github.com/AlphaWallet/alpha-wallet-android/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/AlphaWallet/alpha-wallet-android/actions/workflows/lint.yml) +[![Unit test](https://github.com/AlphaWallet/alpha-wallet-android/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/AlphaWallet/alpha-wallet-android/actions/workflows/ci.yml) +[![E2E Test](https://github.com/AlphaWallet/alpha-wallet-android/actions/workflows/e2e.yml/badge.svg?branch=master)](https://github.com/AlphaWallet/alpha-wallet-android/actions/workflows/e2e.yml) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg )](https://github.com/AlphaWallet/alpha-wallet-android/graphs/commit-activity) ![GitHub contributors](https://img.shields.io/github/contributors/AlphaWallet/alpha-wallet-android.svg) [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/AlphaWallet/alpha-wallet-android/blob/master/LICENSE) -[![codecov](https://codecov.io/gh/AlphaWallet/alpha-wallet-android/branch/master/graph/badge.svg)](https://codecov.io/gh/AlphaWallet/alpha-wallet-android) +[![codecov](https://codecov.io/gh/AlphaWallet/alpha-wallet-android/branch/master/graph/badge.svg?token=IkoEb30CXq)](https://codecov.io/gh/AlphaWallet/alpha-wallet-android) AlphaWallet is an open source programmable blockchain apps platform. It's compatible with tokenisation framework TokenScript, offering businesses and their users in-depth token interaction, a clean white label user experience and advanced security options. Supports all Ethereum based networks. @@ -20,7 +22,7 @@ AlphaWallet and TokenScript have been used by tokenisation projects like FIFA an ## About AlphaWallet - Features Easy to use and secure open source Ethereum wallet for Android and iOS, with native ERC20, ERC721, ERC1155 and ERC875 support. AlphaWallet supports all Ethereum based networks: Ethereum, xDai, Ethereum Classic, Artis, POA, Binance Smart Chain, Heco, Polygon, Avalanche, Fantom, L2 chains Optimistic and Arbitrum, and Palm. -TestChains: Ropsten, Goerli, Kovan, Rinkeby, Sokol, Binance Test, Heco Test, Fuji (Avalanche test), Fantom Test, Polygon Test, Optimistic and Arbitrum Test, Cronos Test and Palm test. +TestChains: Goerli, Binance Test, Heco Test, Fuji (Avalanche test), Fantom Test, Polygon Test, Optimistic and Arbitrum Test, Cronos Test and Palm test. - Beginner Friendly - Secure Enclave Security @@ -62,7 +64,13 @@ We want to give businesses the whitelabel tools they need to develop their ether 1. [Download](https://developer.android.com/studio/) Android Studio. 2. Clone this repository 3. Obtain a free Infura API key and replace the one in build.gradle -4. Build the project in AndroidStudio or Run `./gradlew build` to install tools and dependencies. See [BUILD.md](BUILD.md) for more details. +4. Generate a GitHub [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with `read:packages, read:user` permission +5. Edit `~/.gradle/gradle.properties` add blow properties: +```properties +gpr.user=Your GitHub Email +gpr.key=The GitHub Personal Access Token you created in previous step +``` +6. Build the project in AndroidStudio or Run `./gradlew build` to install tools and dependencies. See [BUILD.md](BUILD.md) for more details. You can also build it from the commandline just like other Android apps. Note that JDK 8 and 11 are the versions supported by Android. diff --git a/app/AlphaWalletStyle.xml b/app/AlphaWalletStyle.xml index 069f4ca6f6..f356da7dc8 100644 --- a/app/AlphaWalletStyle.xml +++ b/app/AlphaWalletStyle.xml @@ -1,4 +1,7 @@ + + @@ -122,4 +126,7 @@ + + \ No newline at end of file diff --git a/app/CMakeCache.txt b/app/CMakeCache.txt new file mode 100644 index 0000000000..6ee713b60b --- /dev/null +++ b/app/CMakeCache.txt @@ -0,0 +1,150 @@ +# This is the CMakeCache file. +# For build in directory: c:/Users/soyle/StudioProjects/x/y/alpha-wallet-android/app +# It was generated by CMake: E:/dev/Android/cmake/3.18.1/bin/cmake.exe +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//No help, variable specified on the command line. +ANDROID_ABI:UNINITIALIZED=x86_64 + +//No help, variable specified on the command line. +ANDROID_NDK:UNINITIALIZED=E:\dev\Android\ndk\21.4.7075529 + +//No help, variable specified on the command line. +ANDROID_PLATFORM:UNINITIALIZED=android-23 + +//No help, variable specified on the command line. +ASKEY:UNINITIALIZED="HFDDY5BNKGXBB82DE2G8S64C3C41B76PYI" -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=C:\Users\soyle\StudioProjects\x\y\alpha-wallet-android\app\build\intermediates\cxx\Debug\3un4y862\obj\x86_64 -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=C:\Users\soyle\StudioProjects\x\y\alpha-wallet-android\app\build\intermediates\cxx\Debug\3un4y862\obj\x86_64 -DCMAKE_BUILD_TYPE=Debug -BC:\Users\soyle\StudioProjects\x\y\alpha-wallet-android\app\.cxx\Debug\3un4y862\x86_64 -GNinja + +//No help, variable specified on the command line. +CMAKE_ANDROID_ARCH_ABI:UNINITIALIZED=x86_64 + +//No help, variable specified on the command line. +CMAKE_ANDROID_NDK:UNINITIALIZED=E:\dev\Android\ndk\21.4.7075529 + +//Archiver +CMAKE_AR:FILEPATH=E:/dev/Android/ndk/21.4.7075529/toolchains/llvm/prebuilt/windows-x86_64/bin/i686-linux-android-ar.exe + +//Flags used by the compiler during all build types. +CMAKE_ASM_FLAGS:STRING= + +//Flags used by the compiler during debug builds. +CMAKE_ASM_FLAGS_DEBUG:STRING= + +//Flags used by the compiler during release builds. +CMAKE_ASM_FLAGS_RELEASE:STRING= + +//Semicolon separated list of supported configuration types, only +// supports Debug, Release, MinSizeRel, and RelWithDebInfo, anything +// else will be ignored. +CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo + +//Flags used by the compiler during all build types. +CMAKE_CXX_FLAGS:STRING= + +//Flags used by the compiler during debug builds. +CMAKE_CXX_FLAGS_DEBUG:STRING= + +//Flags used by the compiler during release builds. +CMAKE_CXX_FLAGS_RELEASE:STRING= + +//Flags used by the compiler during all build types. +CMAKE_C_FLAGS:STRING=-DIFKEY="da3717f25f824cc1baa32d812386d93f" -DOSKEY="..." -DPSKEY=""" + +//Flags used by the compiler during debug builds. +CMAKE_C_FLAGS_DEBUG:STRING= + +//Flags used by the compiler during release builds. +CMAKE_C_FLAGS_RELEASE:STRING= + +//Flags used by the linker. +CMAKE_EXE_LINKER_FLAGS:STRING= + +//No help, variable specified on the command line. +CMAKE_EXPORT_COMPILE_COMMANDS:UNINITIALIZED=ON + +//No help, variable specified on the command line. +CMAKE_MAKE_PROGRAM:UNINITIALIZED=E:\dev\Android\cmake\3.18.1\bin\ninja.exe + +//Flags used by the linker during the creation of modules. +CMAKE_MODULE_LINKER_FLAGS:STRING= + +//Value Computed by CMake +CMAKE_PROJECT_DESCRIPTION:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_HOMEPAGE_URL:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_NAME:STATIC=Project + +//Ranlib +CMAKE_RANLIB:FILEPATH=E:/dev/Android/ndk/21.4.7075529/toolchains/llvm/prebuilt/windows-x86_64/bin/i686-linux-android-ranlib.exe + +//Flags used by the linker during the creation of dll's. +CMAKE_SHARED_LINKER_FLAGS:STRING= + +//No help, variable specified on the command line. +CMAKE_SYSTEM_NAME:UNINITIALIZED=Android + +//No help, variable specified on the command line. +CMAKE_SYSTEM_VERSION:UNINITIALIZED=23 + +//No help, variable specified on the command line. +CMAKE_TOOLCHAIN_FILE:UNINITIALIZED=E:\dev\Android\ndk\21.4.7075529\build\cmake\android.toolchain.cmake + +//Value Computed by CMake +Project_BINARY_DIR:STATIC=C:/Users/soyle/StudioProjects/x/y/alpha-wallet-android/app + +//Value Computed by CMake +Project_SOURCE_DIR:STATIC=C:/Users/soyle/StudioProjects/x/y/alpha-wallet-android/app/src/main/cpp + + +######################## +# INTERNAL cache entries +######################## + +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=c:/Users/soyle/StudioProjects/x/y/alpha-wallet-android/app +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=18 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=1 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=E:/dev/Android/cmake/3.18.1/bin/cmake.exe +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=E:/dev/Android/cmake/3.18.1/bin/cpack.exe +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=E:/dev/Android/cmake/3.18.1/bin/ctest.exe +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Visual Studio 15 2017 +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL=C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=C:/Users/soyle/StudioProjects/x/y/alpha-wallet-android/app/src/main/cpp +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//Platform information initialized +CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=E:/dev/Android/cmake/3.18.1/share/cmake-3.18 + diff --git a/app/CMakeFiles/3.18.1-g262b901-dirty/CMakeSystem.cmake b/app/CMakeFiles/3.18.1-g262b901-dirty/CMakeSystem.cmake new file mode 100644 index 0000000000..c09d28bbb6 --- /dev/null +++ b/app/CMakeFiles/3.18.1-g262b901-dirty/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Windows-10.0.19044") +set(CMAKE_HOST_SYSTEM_NAME "Windows") +set(CMAKE_HOST_SYSTEM_VERSION "10.0.19044") +set(CMAKE_HOST_SYSTEM_PROCESSOR "AMD64") + +include("E:/dev/Android/ndk/21.4.7075529/build/cmake/android.toolchain.cmake") + +set(CMAKE_SYSTEM "Android-1") +set(CMAKE_SYSTEM_NAME "Android") +set(CMAKE_SYSTEM_VERSION "1") +set(CMAKE_SYSTEM_PROCESSOR "i686") + +set(CMAKE_CROSSCOMPILING "TRUE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/app/CMakeFiles/cmake.check_cache b/app/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000000..3dccd73172 --- /dev/null +++ b/app/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/app/build.gradle b/app/build.gradle index 7d6f05a0a8..c116a6a7f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ buildscript { dependencies { classpath "gradle.plugin.com.worker8.android_lint_reporter:android_lint_reporter:2.1.0" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.20.0-RC1" + classpath "com.dicedmelon.gradle:jacoco-android:0.1.5" } } @@ -25,6 +26,30 @@ apply plugin: 'kotlin-android' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'com.worker8.android_lint_reporter' apply plugin: 'io.gitlab.arturbosch.detekt' +apply plugin: 'com.dicedmelon.gradle.jacoco-android' + +jacoco { + toolVersion = "0.8.8" +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] +} + +jacocoAndroidUnitTestReport { + csv.enabled false + html.enabled true + xml.enabled true +} + +jacocoAndroidUnitTestReport { + excludes += [ + '**/*Realm*.*', + '**/Generated*.*', + '**/*_*.*' + ] +} detekt { toolVersion = "1.20.0-RC1" @@ -43,24 +68,28 @@ android_lint_reporter { repositories { maven { - url 'https://maven.google.com' + url = uri("https://maven.pkg.github.com/trustwallet/wallet-core") + credentials { + username = getGitHubUsername() + password = getPAT() + } } } android { compileSdkVersion 32 - buildToolsVersion '32.0.0' + buildToolsVersion '33.0.0' sourceSets { main { } } defaultConfig { - versionCode 199 - versionName "3.58.0" + versionCode 237 + versionName "3.61.0" applicationId "io.stormbird.wallet" - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion 32 testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunnerArguments clearPackageData: 'true' @@ -69,10 +98,13 @@ android { def DEFAULT_INFURA_API_KEY = "\"da3717f25f824cc1baa32d812386d93f\"" def DEFAULT_OPENSEA_API_KEY = "\"...\""; //Put your OpenSea developer API key in here, otherwise you are reliant on the backup NFT fetch method (which usually works ok) def DEFAULT_POLYGONSCAN_API_KEY = "\"\""; //Put your Polygonscan developer API key in here to get access to Polygon/Mumbai token discovery and transactions + def DEFUALT_WALLETCONNECT_PROJECT_ID = "\"40c6071febfd93f4fe485c232a8a4cd9\"" def DEFAULT_AURORA_API_KEY = "\"HFDDY5BNKGXBB82DE2G8S64C3C41B76PYI\""; //Put your Aurorascan.dev API key here - this one will rate limit as it is common buildConfigField 'int', 'DB_VERSION', '45' + buildConfigField "String", XInfuraAPI, DEFAULT_INFURA_API_KEY + buildConfigField "String", "WALLETCONNECT_PROJECT_ID", DEFUALT_WALLETCONNECT_PROJECT_ID ndk { abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" @@ -81,6 +113,8 @@ android { pickFirst 'META-INF/LICENSE.md' pickFirst 'META-INF/NOTICE.md' pickFirst 'META-INF/LICENSE-notice.md' + pickFirst 'META-INF/INDEX.LIST' + pickFirst 'META-INF/DEPENDENCIES' pickFirst 'solidity/ens/build/AbstractENS.bin' } @@ -90,6 +124,7 @@ android { cFlags "-DOSKEY=\\\"" + DEFAULT_OPENSEA_API_KEY + "\\\"" cFlags "-DPSKEY=\\\"" + DEFAULT_POLYGONSCAN_API_KEY + "\\\"" cFlags "-DASKEY=\\\"" + DEFAULT_AURORA_API_KEY + "\\\"" + cFlags "-DWALLETCONNECT_PROJECT_ID=\\\"" + DEFUALT_WALLETCONNECT_PROJECT_ID + "\\\"" } } } @@ -155,8 +190,14 @@ android { abortOnError false } compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 - sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } + dexOptions { + javaMaxHeapSize = "4g" } externalNativeBuild { cmake { @@ -185,6 +226,24 @@ task printVersionCode { } } +gradle.projectsEvaluated({ + def username = getGitHubUsername() + def password = getPAT() + if (!username || !password) { + throw new GradleException('Please provide GitHub username and Personal Access Token. Find more here https://github.com/alphaWallet/alpha-wallet-android#getting-started') + } +}) + +// GitHub Personal Access Token +private String getPAT() { + def encodedToken = project.findProperty("gpr.key") + new String(encodedToken.decodeBase64()) +} + +private String getGitHubUsername() { + project.findProperty("gpr.user") +} + dependencies { implementation project(":lib") @@ -198,8 +257,8 @@ dependencies { // Ethereum client //implementation "org.web3j:core:4.8.8-android" implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.fasterxml.jackson.core:jackson-core:2.13.2' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + implementation 'com.fasterxml.jackson.core:jackson-core:2.13.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' implementation 'org.slf4j:slf4j-api:2.0.0-alpha7' // Http client @@ -211,7 +270,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' //noinspection GradleDependency,GradleCompatible implementation 'androidx.appcompat:appcompat:1.3.1' //Do not update; next version is incompatible with API30 and below - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.biometric:biometric:1.1.0' @@ -219,12 +278,10 @@ dependencies { // Bar code scanning implementation 'com.journeyapps:zxing-android-embedded:4.3.0' - implementation 'com.google.zxing:core:3.4.1' + implementation 'com.google.zxing:core:3.5.0' // Sugar - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation 'com.github.apl-devs:appintro:v4.2.2' - implementation 'com.romandanylyk:pageindicatorview:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' //coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5' @@ -245,7 +302,7 @@ dependencies { annotationProcessor "com.google.dagger:hilt-compiler:2.40.5" // WebKit - for WebView Dark Mode - implementation 'androidx.webkit:webkit:1.4.0' + implementation 'androidx.webkit:webkit:1.5.0' //Use Leak Canary for debug builds only //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' @@ -262,16 +319,17 @@ dependencies { testImplementation group: 'org.powermock', name: 'powermock-module-junit4-rule-agent', version: '2.0.9' testImplementation group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.9' testImplementation group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.9' + testImplementation group: 'org.json', name: 'json', version: '20220320' // Component tests - testImplementation 'org.robolectric:robolectric:4.8' + testImplementation 'org.robolectric:robolectric:4.8.2' testImplementation 'androidx.test:core:1.4.0' testImplementation 'androidx.test.ext:junit:1.1.3' // E2e tests - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:core:1.4.0' - androidTestUtil 'androidx.test:orchestrator:1.4.1' + androidTestImplementation 'androidx.test:runner:1.5.0-alpha02' + androidTestImplementation 'androidx.test:core:1.4.1-alpha05' + androidTestUtil 'androidx.test:orchestrator:1.4.2-alpha02' androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', { exclude group: "com.android.support", module: "support-annotations" }) @@ -282,13 +340,14 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0' + androidTestImplementation 'androidx.browser:browser:1.4.0' - implementation group: 'com.trustwallet', name: 'wallet-core', version: '2.6.3' + implementation 'com.trustwallet:wallet-core:2.6.4' - implementation 'com.github.florent37:tutoshowcase:1.0.1' + implementation 'com.github.florent37:TutoShowcase:d8b91be8a2' // Do not upgrade unless we have migrated to AndroidX - implementation 'com.google.android:flexbox:2.0.1' + implementation 'com.github.google:flexbox-layout:2.0.1' implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' @@ -298,13 +357,19 @@ dependencies { //Timber implementation 'com.jakewharton.timber:timber:5.0.1' + implementation('com.walletconnect:auth:1.1.0', { + exclude group: 'org.web3j', module: '*' + }) + implementation 'com.walletconnect:sign:2.1.0' + implementation 'com.walletconnect:android-core:1.3.0' + implementation 'androidx.work:work-runtime:2.7.1' //Analytics analyticsImplementation 'com.google.android.play:core:1.10.3' - analyticsImplementation 'com.google.firebase:firebase-analytics:20.1.2' + analyticsImplementation 'com.google.firebase:firebase-analytics:21.1.1' analyticsImplementation 'com.mixpanel.android:mixpanel-android:5.8.4' - analyticsImplementation 'com.google.firebase:firebase-crashlytics:18.2.9' + analyticsImplementation 'com.google.firebase:firebase-crashlytics:18.2.13' } // WARNING WARNING WARNING diff --git a/app/check/detekt-baseline.xml b/app/check/detekt-baseline.xml index 322db15915..7b1a54b655 100644 --- a/app/check/detekt-baseline.xml +++ b/app/check/detekt-baseline.xml @@ -1,18 +1,15 @@ - + - + ComplexCondition:WCSession.kt$WCSession.Companion$bridge == null || key == null || topic == null || version == null - ComplexMethod:WCClient.kt$WCClient$private fun handleRequest(request: JsonRpcRequest<JsonArray>) - FunctionNaming:SuggestEIP1559.kt$fun SuggestEIP1559(gasService: GasService, feeHistory: FeeHistory): Single<MutableMap<Int, EIP1559FeeOracleResult>> + ComplexMethod:WCClient.kt$WCClient$private fun handleRequest(request: JsonRpcRequest<JsonArray>) + EmptyFunctionBlock:ShadowWalletConnectClient.kt$ShadowWalletConnectClient${} + EmptySecondaryConstructor:ShadowWalletConnectClient.kt$ShadowWalletConnectClient${} + FunctionNaming:SuggestEIP1559.kt$fun SuggestEIP1559(gasService: GasService, feeHistory: FeeHistory): Single<MutableMap<Int, EIP1559FeeOracleResult>> FunctionParameterNaming:SuggestEIP1559.kt$_needBlocks: Int FunctionParameterNaming:SuggestEIP1559.kt$_ptr: Int FunctionParameterNaming:WCClient.kt$WCClient$_chainId: Long - MagicNumber:JsonRpcModels.kt$JsonRpcError.Companion$32000 - MagicNumber:JsonRpcModels.kt$JsonRpcError.Companion$32600 - MagicNumber:JsonRpcModels.kt$JsonRpcError.Companion$32601 - MagicNumber:JsonRpcModels.kt$JsonRpcError.Companion$32602 - MagicNumber:JsonRpcModels.kt$JsonRpcError.Companion$32700 MagicNumber:SuggestEIP1559.kt$0.9 MagicNumber:SuggestEIP1559.kt$100.0 MagicNumber:SuggestEIP1559.kt$16 @@ -25,7 +22,7 @@ MaxLineLength:SuggestEIP1559.kt$// If a narrower time window yields a lower base fee suggestion than a wider window then we are probably in a price dip. MaxLineLength:SuggestEIP1559.kt$// In this case getting included with a low priority fee is not guaranteed; instead we use the higher base fee suggestion MaxLineLength:SuggestEIP1559.kt$// feeHistory API call with reward percentile specified is expensive and therefore is only requested for a few non-full recent blocks. - MaxLineLength:SuggestEIP1559.kt$else -> (1 - cos((percentile - sampleMinPercentile) * 2 * Math.PI / (sampleMaxPercentile - sampleMinPercentile))) / 2 + MaxLineLength:SuggestEIP1559.kt$else -> (1 - cos((percentile - sampleMinPercentile) * 2 * Math.PI / (sampleMaxPercentile - sampleMinPercentile))) / 2 MaxLineLength:SuggestEIP1559.kt$internal MaxLineLength:SuggestEIP1559.kt$private MaxLineLength:SuggestEIP1559.kt$private const val rewardBlockPercentile = 40 // suggested priority fee to be selected from sorted individual block reward percentiles @@ -33,7 +30,6 @@ MaxLineLength:SuggestEIP1559.kt$result += ((samplingCurveValue - samplingCurveLast) * baseFee[order[i]].toDouble()).toBigDecimal().toBigInteger() MaxLineLength:SuggestEIP1559.kt$return MaxLineLength:SuggestEIP1559.kt$val feeHistory = gasService.getChainFeeHistory(blockCount, "0x" + (firstBlock + ptr).toString(16), rewardPercentile.toString()).blockingGet() - MaxLineLength:WCClient.kt$WCClient$fun NewLineAtEndOfFile:Enums.kt$com.alphawallet.app.walletconnect.entity.Enums.kt NewLineAtEndOfFile:EthereumModels.kt$com.alphawallet.app.walletconnect.entity.EthereumModels.kt NewLineAtEndOfFile:Exceptions.kt$com.alphawallet.app.walletconnect.entity.Exceptions.kt @@ -41,12 +37,13 @@ NewLineAtEndOfFile:JsonRpcModels.kt$com.alphawallet.app.walletconnect.entity.JsonRpcModels.kt NewLineAtEndOfFile:ReleaseTree.kt$com.alphawallet.app.util.ReleaseTree.kt NewLineAtEndOfFile:SessionModels.kt$com.alphawallet.app.walletconnect.entity.SessionModels.kt + NewLineAtEndOfFile:ShadowWalletConnectClient.kt$com.alphawallet.shadows.ShadowWalletConnectClient.kt NewLineAtEndOfFile:SuggestEIP1559.kt$com.alphawallet.app.entity.SuggestEIP1559.kt NewLineAtEndOfFile:WCSession.kt$com.alphawallet.app.walletconnect.WCSession.kt - ReturnCount:SuggestEIP1559.kt$internal fun predictMinBaseFee(baseFee: Array<BigInteger>, order: List<Int>, timeFactor: Double): BigInteger + ReturnCount:SuggestEIP1559.kt$internal fun predictMinBaseFee(baseFee: Array<BigInteger>, order: List<Int>, timeFactor: Double): BigInteger ReturnCount:WCSession.kt$WCSession.Companion$fun from(from: String): WCSession? SwallowedException:WCClient.kt$WCClient$e: JsonSyntaxException - ThrowsCount:WCClient.kt$WCClient$private fun handleRequest(request: JsonRpcRequest<JsonArray>) + ThrowsCount:WCClient.kt$WCClient$private fun handleRequest(request: JsonRpcRequest<JsonArray>) TooGenericExceptionCaught:WCClient.kt$WCClient$e: Exception TooManyFunctions:WCClient.kt$WCClient : WebSocketListener TopLevelPropertyNaming:SuggestEIP1559.kt$private const val extraPriorityFeeRatio = 0.25 // extra priority fee offered in case of expected baseFee rise @@ -56,7 +53,8 @@ TopLevelPropertyNaming:SuggestEIP1559.kt$private const val rewardPercentile = 10 // effective reward value to be selected from each individual block TopLevelPropertyNaming:SuggestEIP1559.kt$private const val sampleMaxPercentile = 30 TopLevelPropertyNaming:SuggestEIP1559.kt$private const val sampleMinPercentile = 10 // sampled percentile range of exponentially weighted baseFee history - UnusedPrivateMember:WCClient.kt$WCClient$private val TAG = WCClient::class.java.simpleName + UnusedPrivateMember:ShadowWalletConnectClient.kt$ShadowWalletConnectClient$delegate: WalletConnectClient.WalletDelegate + UnusedPrivateMember:ShadowWalletConnectClient.kt$ShadowWalletConnectClient$onError: (WalletConnectException) -> Unit = {} VariableNaming:WCClient.kt$WCClient$private val TAG = WCClient::class.java.simpleName WildcardImport:WCClient.kt$import com.alphawallet.app.walletconnect.entity.* WildcardImport:WCClient.kt$import okhttp3.* diff --git a/app/check/lint-baseline.xml b/app/check/lint-baseline.xml index ca22c68748..b099630d8b 100644 --- a/app/check/lint-baseline.xml +++ b/app/check/lint-baseline.xml @@ -2529,259 +2529,6 @@ column="42"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - implements AnalyticsServiceType { +import timber.log.Timber; +public class AnalyticsService implements AnalyticsServiceType +{ private final MixpanelAPI mixpanelAPI; private final FirebaseAnalytics firebaseAnalytics; - - public static native String getAnalyticsKey(); - - static { - System.loadLibrary("keys"); - } + private final SharedPreferenceRepository preferenceRepository; public AnalyticsService(Context context) { - mixpanelAPI = MixpanelAPI.getInstance(context, getAnalyticsKey()); + mixpanelAPI = MixpanelAPI.getInstance(context, KeyProviderFactory.get().getAnalyticsKey()); firebaseAnalytics = FirebaseAnalytics.getInstance(context); + preferenceRepository = new SharedPreferenceRepository(context); } - @Override - public void track(String eventName) + public static Bundle jsonToBundle(JSONObject jsonObject) throws JSONException { - //firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, eventName); - mixpanelAPI.track(eventName); + Bundle bundle = new Bundle(); + Iterator it = jsonObject.keys(); + while (it.hasNext()) + { + String key = it.next(); + String value = jsonObject.getString(key); + bundle.putString(key, value); + } + return bundle; } @Override - public void track(String eventName, T event) + public void track(String eventName) { - AnalyticsProperties analyticsProperties = (AnalyticsProperties) event; + if (preferenceRepository.isAnalyticsEnabled()) + { + //firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, eventName); - trackFirebase(analyticsProperties, eventName); - trackMixpanel(analyticsProperties, eventName); + mixpanelAPI.track(eventName); + } } - private void trackFirebase(AnalyticsProperties analyticsProperties, String eventName) + @Override + public void track(String eventName, T event) { - Bundle props = new Bundle(); - if(!TextUtils.isEmpty(analyticsProperties.getWalletType())) - { - props.putString(C.AN_WALLET_TYPE, analyticsProperties.getWalletType()); - } - - if(!TextUtils.isEmpty(analyticsProperties.getData())) + if (preferenceRepository.isAnalyticsEnabled()) { - props.putString(C.AN_USE_GAS, analyticsProperties.getData()); + AnalyticsProperties analyticsProperties = (AnalyticsProperties) event; + trackFirebase(analyticsProperties, eventName); + trackMixpanel(analyticsProperties, eventName); } - - props.putString(C.APP_NAME, BuildConfig.APPLICATION_ID); - - firebaseAnalytics.logEvent(eventName, props); } - private void trackMixpanel(AnalyticsProperties analyticsProperties, String eventName) + private void trackFirebase(AnalyticsProperties analyticsProperties, String eventName) { - try + if (preferenceRepository.isAnalyticsEnabled()) { - JSONObject props = new JSONObject(); - - if (!TextUtils.isEmpty(analyticsProperties.getWalletType())) + Bundle props; + try { - props.put(C.AN_WALLET_TYPE, analyticsProperties.getWalletType()); + props = jsonToBundle(analyticsProperties.get()); + props.putString(C.APP_NAME, BuildConfig.APPLICATION_ID); + firebaseAnalytics.logEvent(eventName, props); } - - if (!TextUtils.isEmpty(analyticsProperties.getData())) + catch (JSONException e) { - props.put(C.AN_USE_GAS, analyticsProperties.getData()); + Timber.e(e); } - - mixpanelAPI.track(eventName, props); } - catch(JSONException e) + } + + private void trackMixpanel(AnalyticsProperties analyticsProperties, String eventName) + { + if (preferenceRepository.isAnalyticsEnabled()) { - //Something went wrong + mixpanelAPI.track(eventName, analyticsProperties.get()); } } @Override public void identify(String uuid) { - firebaseAnalytics.setUserId(uuid); - mixpanelAPI.identify(uuid); - mixpanelAPI.getPeople().identify(uuid); - FirebaseInstanceId.getInstance().getInstanceId() + if (preferenceRepository.isAnalyticsEnabled()) + { + firebaseAnalytics.setUserId(uuid); + mixpanelAPI.identify(uuid); + mixpanelAPI.getPeople().identify(uuid); + FirebaseInstanceId.getInstance().getInstanceId() .addOnCompleteListener(task -> { if (task.isSuccessful()) { @@ -107,6 +112,7 @@ public void identify(String uuid) mixpanelAPI.getPeople().setPushRegistrationId(token); } }); + } } @Override @@ -119,6 +125,9 @@ public void flush() @Override public void recordException(ServiceErrorException e) { - FirebaseCrashlytics.getInstance().recordException(e); + if (preferenceRepository.isCrashReportingEnabled()) + { + FirebaseCrashlytics.getInstance().recordException(e); + } } } \ No newline at end of file diff --git a/app/src/analytics/java/com/alphawallet/app/util/UpdateUtils.java b/app/src/analytics/java/com/alphawallet/app/util/UpdateUtils.java index d928006dbe..f3c746e090 100644 --- a/app/src/analytics/java/com/alphawallet/app/util/UpdateUtils.java +++ b/app/src/analytics/java/com/alphawallet/app/util/UpdateUtils.java @@ -20,7 +20,7 @@ public static void checkForUpdates(Activity context, FragmentMessenger messenger appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> { if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { - messenger.updateReady(appUpdateInfo.availableVersionCode()); + messenger.playStoreUpdateReady(appUpdateInfo.availableVersionCode()); } }); } diff --git a/app/src/androidTest/java/com/alphawallet/app/AnalyticsSettingsTest.java b/app/src/androidTest/java/com/alphawallet/app/AnalyticsSettingsTest.java new file mode 100644 index 0000000000..28c15b5b25 --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/AnalyticsSettingsTest.java @@ -0,0 +1,33 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.gotoSettingsPage; +import static com.alphawallet.app.steps.Steps.selectMenu; +import static com.alphawallet.app.util.Helper.click; + +import org.junit.Test; + +public class AnalyticsSettingsTest extends BaseE2ETest +{ + @Test + public void title_should_see_analytics_settings_page() + { + createNewWallet(); + gotoSettingsPage(); + selectMenu("Advanced"); + click(withText("Analytics")); + shouldSee("Share Anonymous Data"); + } + + @Test + public void title_should_see_crash_report_settings_page() + { + createNewWallet(); + gotoSettingsPage(); + selectMenu("Advanced"); + click(withText("Crash Reporting")); + shouldSee("Share Anonymous Data"); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/BaseE2ETest.java b/app/src/androidTest/java/com/alphawallet/app/BaseE2ETest.java index 0fa415d678..7da0085ce6 100644 --- a/app/src/androidTest/java/com/alphawallet/app/BaseE2ETest.java +++ b/app/src/androidTest/java/com/alphawallet/app/BaseE2ETest.java @@ -2,31 +2,65 @@ import static androidx.test.espresso.Espresso.setFailureHandler; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.alphawallet.app.steps.Steps.closeSecurityWarning; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject; +import androidx.test.uiautomator.UiObjectNotFoundException; +import androidx.test.uiautomator.UiSelector; import com.alphawallet.app.ui.SplashActivity; import com.alphawallet.app.util.CustomFailureHandler; +import org.junit.Before; import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runner.RunWith; +import timber.log.Timber; + @RunWith(AndroidJUnit4.class) public abstract class BaseE2ETest { @Rule - public TestRule watcher = new TestWatcher() { - protected void starting(Description description) { - setFailureHandler(new CustomFailureHandler(description.getMethodName(), InstrumentationRegistry.getInstrumentation().getTargetContext())); + public TestRule watcher = new TestWatcher() + { + protected void starting(Description description) + { + setFailureHandler(new CustomFailureHandler(description.getMethodName(), getInstrumentation().getTargetContext())); } }; @Rule public ActivityScenarioRule activityScenarioRule = new ActivityScenarioRule<>(SplashActivity.class); + + @Before + public void setUp() + { + dismissANRSystemDialog(); + closeSecurityWarning(); + } + + private void dismissANRSystemDialog() + { + UiDevice device = UiDevice.getInstance(getInstrumentation()); + UiObject waitButton = device.findObject(new UiSelector().textContains("wait")); + if (waitButton.exists()) + { + try + { + waitButton.click(); + } + catch (UiObjectNotFoundException e) + { + Timber.e(e); + } + } + } } diff --git a/app/src/androidTest/java/com/alphawallet/app/CoinbasePayTest.java b/app/src/androidTest/java/com/alphawallet/app/CoinbasePayTest.java new file mode 100644 index 0000000000..fea044df3a --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/CoinbasePayTest.java @@ -0,0 +1,24 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.util.Helper.click; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; + +public class CoinbasePayTest extends BaseE2ETest +{ + @Test + public void should_see_coinbase_pay_window() + { + createNewWallet(); + click(withText("Buy ETH")); + shouldSee("Buy with Coinbase Pay"); + click(withId(R.id.buy_with_coinbase_pay)); + shouldSee("Buy with Coinbase Pay"); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/CurrencyTest.java b/app/src/androidTest/java/com/alphawallet/app/CurrencyTest.java new file mode 100644 index 0000000000..bfb84d1dce --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/CurrencyTest.java @@ -0,0 +1,27 @@ +package com.alphawallet.app; + +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.gotoWalletPage; +import static com.alphawallet.app.steps.Steps.selectCurrency; + +import org.junit.Test; + +public class CurrencyTest extends BaseE2ETest +{ + + @Test + public void should_switch_currency() + { + createNewWallet(); + + selectCurrency("CNY"); + gotoWalletPage(); + shouldSee("¥"); + + selectCurrency("IDR"); + gotoWalletPage(); + shouldSee("Rp"); + } + +} diff --git a/app/src/androidTest/java/com/alphawallet/app/DappBrowserTest.java b/app/src/androidTest/java/com/alphawallet/app/DappBrowserTest.java index add8b33be1..a07d7a709e 100644 --- a/app/src/androidTest/java/com/alphawallet/app/DappBrowserTest.java +++ b/app/src/androidTest/java/com/alphawallet/app/DappBrowserTest.java @@ -2,35 +2,148 @@ import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.Espresso.pressBack; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.pressImeActionButton; +import static androidx.test.espresso.action.ViewActions.pressKey; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldNotSee; +import static com.alphawallet.app.assertions.Should.shouldSee; import static com.alphawallet.app.steps.Steps.createNewWallet; import static com.alphawallet.app.steps.Steps.navigateToBrowser; +import static com.alphawallet.app.steps.Steps.openOptionsMenu; import static com.alphawallet.app.steps.Steps.selectTestNet; -import static com.alphawallet.app.steps.Steps.visit; -import static com.alphawallet.app.util.Helper.waitUntil; +import static com.alphawallet.app.util.Helper.click; +import static com.alphawallet.app.util.Helper.waitUntilLoaded; +import static junit.framework.TestCase.assertTrue; + +import static org.hamcrest.core.IsNot.not; + +import android.view.KeyEvent; + +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.espresso.action.ViewActions; import com.alphawallet.app.util.Helper; -import org.junit.Ignore; +import org.junit.Before; import org.junit.Test; public class DappBrowserTest extends BaseE2ETest { + private static final String DEFAULT_HOME_PAGE = "https://courses.cs.washington.edu/courses/cse373/99sp/assignments/hw2/test1.html"; + private static final String URL_DAPP = "http://web.simmons.edu/~grovesd/comm244/notes/week3/html-test-page.html"; + + @Override + @Before + public void setUp() + { + super.setUp(); + createNewWallet(); + visit(DEFAULT_HOME_PAGE); + } @Test - @Ignore public void should_switch_network() { - String urlString = "https://opensea.io"; - - createNewWallet(); - visit(urlString); - onView(isRoot()).perform(waitUntil(withText("Ethereum"), 60)); - selectTestNet(); + shouldSee("Ethereum"); + selectTestNet("Görli"); navigateToBrowser(); - Helper.wait(3); pressBack(); - onView(isRoot()).perform(waitUntil(withText("Kovan"), 60)); + waitUntilLoaded(); + shouldSee("Görli"); + } + + @Test + public void should_suggest_websites() + { + onView(withId(R.id.url_tv)).perform(click()); + click(withId(R.id.clear_url)); + onView(withId(R.id.url_tv)).perform(pressKey(KeyEvent.KEYCODE_R), pressKey(KeyEvent.KEYCODE_A)); + Helper.wait(2); + onView(withId(R.id.url_tv)).perform(pressKey(KeyEvent.KEYCODE_TAB), pressKey(KeyEvent.KEYCODE_DPAD_DOWN)); + waitUntilLoaded(); + onView(withId(R.id.url_tv)).perform(pressKey(KeyEvent.KEYCODE_ENTER)); + assertUrlContains("alphawallet.app.entity.DApp@"); + } + + @Test + public void should_go_back_when_press_back_button_on_phone() + { + visit(URL_DAPP); + assertUrlContains(URL_DAPP); + Helper.wait(2); + pressBack(); + waitUntilLoaded(); + assertUrlContains(DEFAULT_HOME_PAGE); + } + + @Test + public void should_navigate_forward_or_backward() + { + visit(URL_DAPP); + assertUrlContains(URL_DAPP); + Helper.wait(2); + click(withId(R.id.back)); + waitUntilLoaded(); + assertUrlContains(DEFAULT_HOME_PAGE); + Helper.wait(2); + click(withId(R.id.next)); + waitUntilLoaded(); + assertUrlContains(URL_DAPP); + } + + @Test + public void should_clear_url_and_show_keyboard() + { + assertUrlContains(DEFAULT_HOME_PAGE); + onView(withId(R.id.url_tv)).perform(click()); + click(withId(R.id.clear_url)); + assertUrlContains(""); + assertTrue(Helper.isSoftKeyboardShown(ApplicationProvider.getApplicationContext())); + } + + @Test + public void should_hide_buttons_when_typing_url() + { + shouldSee(R.id.back); + shouldSee(R.id.next); + + onView(withId(R.id.url_tv)).perform(ViewActions.click()); + + shouldNotSee(R.id.back); + shouldNotSee(R.id.next); + } + + @Test + public void should_set_homepage() + { + visit(URL_DAPP); + waitUntilLoaded(); + openOptionsMenu(); + Helper.wait(1); + click(withText("Set as Home Page")); + Helper.wait(2); + click(withId(R.id.home)); + waitUntilLoaded(); + assertUrlContains(URL_DAPP); + } + + @NonNull + private void visit(String url) + { + navigateToBrowser(); + onView(withId(R.id.url_tv)).perform(replaceText(url), pressImeActionButton()); + waitUntilLoaded(); + } + + @NonNull + private void assertUrlContains(String expectedUrl) + { + onView(withId(R.id.url_tv)).check(matches(withSubstring(expectedUrl))); } } diff --git a/app/src/androidTest/java/com/alphawallet/app/I18nTest.java b/app/src/androidTest/java/com/alphawallet/app/I18nTest.java new file mode 100644 index 0000000000..7f2f25d1ff --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/I18nTest.java @@ -0,0 +1,45 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.Espresso.pressBack; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.gotoSettingsPage; +import static com.alphawallet.app.steps.Steps.selectMenu; +import static com.alphawallet.app.util.Helper.click; + +import org.junit.Test; + +public class I18nTest extends BaseE2ETest +{ + + @Test + public void should_switch_language() + { + createNewWallet(); + gotoSettingsPage(); + + selectMenu("Change Language"); + click(withText("Chinese")); + pressBack(); + + selectMenu("更换语言"); + click(withText("西班牙语")); + pressBack(); + + selectMenu("Cambiar idioma"); + click(withText("Francés")); + pressBack(); + + selectMenu("Changer Langue"); + click(withText("Vietnamien")); + pressBack(); + + selectMenu("Thay đổi ngôn ngữ"); + click(withText("Tiếng Miến Điện")); + pressBack(); + + selectMenu("ဘာသာစကားပြောင်းမည်"); + click(withText("အင်ဒိုနီးရှား")); + pressBack(); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/ImportWalletWithSeedPhraseTest.java b/app/src/androidTest/java/com/alphawallet/app/ImportWalletWithSeedPhraseTest.java new file mode 100644 index 0000000000..79b9f16143 --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/ImportWalletWithSeedPhraseTest.java @@ -0,0 +1,51 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withParent; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.steps.Steps.closeSecurityWarning; +import static com.alphawallet.app.steps.Steps.closeSelectNetworkPage; +import static com.alphawallet.app.steps.Steps.getWalletAddress; +import static com.alphawallet.app.util.Helper.click; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.fail; + +import android.os.Build; + +import com.alphawallet.app.util.Helper; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class ImportWalletWithSeedPhraseTest extends BaseE2ETest { + private static final Map WALLETS = new HashMap() {{ + put("24", new String[]{"essence allow crisp figure tired task melt honey reduce planet twenty rookie", "0xD0c424B3016E9451109ED97221304DeC639b3F84"}); + put("30", new String[]{"deputy review citizen bacon measure combine bag dose chronic retreat attack fly", "0xD8790c1eA5D15F8149C97F80524AC87f56301204"}); + put("32", new String[]{"omit mobile upgrade warm flock two era hamster local cat wink virus", "0x32f6F38137a79EA8eA237718b0AFAcbB1c58ca2e"}); + }}; + + @Test + public void should_import_wallet_with_seed_phrase() { + int apiLevel = Build.VERSION.SDK_INT; + String[] array = WALLETS.get(String.valueOf(apiLevel)); + if (array == null) { + fail("Please config seed phrase and wallet address for this API level first."); + } + + String seedPhrase = array[0]; + String existedWalletAddress = array[1]; + + click(withText("I already have a Wallet")); + + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_seed)))))).perform(replaceText(seedPhrase)); + Helper.wait(2); // Avoid error: Error performing a ViewAction! soft keyboard dismissal animation may have been in the way. Retrying once after: 1000 millis + click(withId(R.id.import_action)); + assertThat(getWalletAddress(), equalTo(existedWalletAddress)); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/KeyServiceTest.java b/app/src/androidTest/java/com/alphawallet/app/KeyServiceTest.java new file mode 100644 index 0000000000..7e9be38e7d --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/KeyServiceTest.java @@ -0,0 +1,70 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.closeSecurityWarning; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.gotoSettingsPage; +import static com.alphawallet.app.steps.Steps.importKSWalletFromFrontPage; +import static com.alphawallet.app.util.Helper.click; +import static org.junit.Assert.fail; + +import android.os.Build; + +import com.alphawallet.app.util.Helper; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class KeyServiceTest extends BaseE2ETest { + private static final String keystore = "{\"address\":\"f9c883c8dca140ebbdc87a225fe6e330be5d25ef\",\"id\":\"5648908b-1862-4f3e-b425-d1ba0790a601\",\"version\":3,\"crypto\":{\"cipher\":\"aes-128-ctr\",\"cipherparams\":{\"iv\":\"bcbfcffb52f42e9d149b97a8512d4c49\"},\"ciphertext\":\"967d3cd0db82445e4e74a6d5e537c799632e91cf0ca6f9fec17c769812e9454f\",\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"n\":4096,\"p\":6,\"r\":8,\"salt\":\"084c44b6e76e2b879257520ac00bb59c93e17321ce4a029f9b8294e304defc7a\"},\"mac\":\"0e4a74746d0c3e2739653200bcffb92716e77677d41f84c1184a4eb2054963c6\"}}\n"; + private static final String password = "hellohello"; + + @Test + public void cipher_integrity_test() { + createNewWallet(); + gotoSettingsPage(); + + click(withText("Change / Add Wallet")); + click(withId(R.id.manage_wallet_btn)); + click(withId(R.id.action_key_status)); + + click(withText("Run Key Diagnostic")); + + Helper.wait(1); + + //now check the key is decoded correctly + shouldSee("Key found"); + shouldSee("Unlocked"); + shouldSee("Seed Phrase detected public key"); + shouldSee("HDKEY"); + } + + @Test + public void cipher_integrity_test_keystore() { + importKSWalletFromFrontPage(keystore, password); + + gotoSettingsPage(); + click(withText("Change / Add Wallet")); + Helper.wait(1); + click(withId(R.id.manage_wallet_btn)); + Helper.wait(1); + + click(withId(R.id.action_key_status)); + + Helper.wait(1); + + click(withText("Run Key Diagnostic")); + + Helper.wait(1); + + //now check the key is decoded correctly + shouldSee("Key found"); + shouldSee("Unlocked"); + shouldSee("Decoded Keystore public key"); + shouldSee("KEYSTORE"); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/ManageNetworkTest.java b/app/src/androidTest/java/com/alphawallet/app/ManageNetworkTest.java deleted file mode 100644 index f1c0b79e43..0000000000 --- a/app/src/androidTest/java/com/alphawallet/app/ManageNetworkTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.alphawallet.app; - -import static com.alphawallet.app.assertions.Should.shouldSee; -import static com.alphawallet.app.steps.Steps.addNewNetwork; -import static com.alphawallet.app.steps.Steps.createNewWallet; -import static com.alphawallet.app.steps.Steps.gotoSettingsPage; - -import org.junit.Ignore; -import org.junit.Test; - -public class ManageNetworkTest extends BaseE2ETest -{ - @Test - @Ignore - public void should_add_custom_network() - { - createNewWallet(); - gotoSettingsPage(); - addNewNetwork("MyTestNet"); - shouldSee("MyTestNet"); - } -} diff --git a/app/src/androidTest/java/com/alphawallet/app/SelectNetworkTest.java b/app/src/androidTest/java/com/alphawallet/app/SelectNetworkTest.java new file mode 100644 index 0000000000..b33740433c --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/SelectNetworkTest.java @@ -0,0 +1,24 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.gotoSettingsPage; +import static com.alphawallet.app.steps.Steps.selectMenu; +import static com.alphawallet.app.util.Helper.clickListItem; + +import org.junit.Test; + +public class SelectNetworkTest extends BaseE2ETest +{ + @Test + public void title_should_update_count() + { + createNewWallet(); + gotoSettingsPage(); + selectMenu("Select Active Networks"); + shouldSee("Enabled Networks (1)"); + clickListItem(R.id.test_list, withSubstring("Gnosis")); + shouldSee("Enabled Networks (2)"); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/SwapTest.java b/app/src/androidTest/java/com/alphawallet/app/SwapTest.java new file mode 100644 index 0000000000..bf5fa26e98 --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/SwapTest.java @@ -0,0 +1,40 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.Espresso.pressBack; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withParent; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.util.Helper.click; +import static org.hamcrest.CoreMatchers.allOf; + +import com.alphawallet.app.util.Helper; + +import org.junit.Test; + +public class SwapTest extends BaseE2ETest +{ + @Test + public void should_see_swap_window() + { + createNewWallet(); + click(withText("0 ETH")); + click(withId(R.id.more_button)); + click(withText("Swap")); + shouldSee("Select Exchanges"); + click(withText("DODO")); + pressBack(); + Helper.wait(5); + click(allOf(withId(R.id.chain_name), withParent(withId(R.id.layout_chain_name)))); + click(withText("1%")); + shouldSee("DODO"); + click(withText("Edit")); + click(withText("1inch")); + pressBack(); + shouldSee("1inch"); + pressBack(); + click(withId(R.id.action_settings)); + shouldSee("Settings"); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/TokenScriptCertificateTest.java b/app/src/androidTest/java/com/alphawallet/app/TokenScriptCertificateTest.java new file mode 100644 index 0000000000..b609e61eb3 --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/TokenScriptCertificateTest.java @@ -0,0 +1,154 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.Espresso.pressBack; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.GANACHE_URL; +import static com.alphawallet.app.steps.Steps.addNewNetwork; +import static com.alphawallet.app.steps.Steps.getWalletAddress; +import static com.alphawallet.app.steps.Steps.importPKWalletFromFrontPage; +import static com.alphawallet.app.steps.Steps.selectTestNet; +import static com.alphawallet.app.steps.Steps.switchToWallet; +import static com.alphawallet.app.util.Helper.click; +import static com.alphawallet.app.util.Helper.waitUntil; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.AllOf.allOf; +import static org.junit.Assert.fail; + +import android.os.Build; + +import androidx.test.espresso.action.ViewActions; + +import com.alphawallet.app.util.EthUtils; +import com.alphawallet.app.util.Helper; +import com.alphawallet.app.resources.Contracts; + +import org.junit.Test; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Keys; +import org.web3j.protocol.Web3j; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by JB on 1/09/2022. + */ +public class TokenScriptCertificateTest extends BaseE2ETest +{ + private String doorContractAddress; + private final String contractOwnerPk = "0x69c22d654be7fe75e31fbe26cb56c93ec91144fab67cb71529c8081971635069"; + private final Web3j web3j; + + private final boolean useMumbai = false; //for local testing + + private static final Map WALLETS_ON_GANACHE = new HashMap() + { + { + put("24", new String[]{"0x644022aef70ad515ee186345fd74b005d759f41be8157c2835de3597d943146d", "0xE494323823fdF1A1Ab6ca79d2538C7182690D52a"}); + put("30", new String[]{"0x5c8843768e0e1916255def80ae7f6197e1f6a2dbcba720038748fc7634e5cffd", "0x162f5e0b63646AAA33a85eA13346F15C5289f901"}); + put("32", new String[]{"0x992b442eaa34de3c6ba0b61c75b2e4e0241d865443e313c4fa6ab8ba488a6957", "0xd7Ba01f596a7cc926b96b3B0a037c47A22904c06"}); + } + }; + + public TokenScriptCertificateTest() + { + int apiLevel = Build.VERSION.SDK_INT; + String[] array = WALLETS_ON_GANACHE.get(String.valueOf(apiLevel)); + + if (array == null) + { + fail("Please config seed phrase and wallet address for this API level first."); + } + + web3j = EthUtils.buildWeb3j(GANACHE_URL); + + String privateKey = array[0]; + + //create credentials for initial transfer + Credentials credentials = Credentials.create(privateKey); + + //create credentials for contract deployment (fixed so we can link to a tokenscript) + Credentials deployCredentials = Credentials.create(contractOwnerPk); + + //Transfer 1 eth into deployment wallet + EthUtils.transferFunds(web3j, credentials, deployCredentials.getAddress(), BigDecimal.ONE); + + //Deploy door contract + EthUtils.deployContract(web3j, deployCredentials, Contracts.doorContractCode); + + //Always use zero nonce for determining the contract address + doorContractAddress = EthUtils.calculateContractAddress(deployCredentials.getAddress(), 0L); + + if (useMumbai) + { + //If using Mumbai for this test: + doorContractAddress = "0xA0343dfd68FcD7F18153b8AB87936c5A9C1Da20e"; + } + } + + @Test + public void should_view_signature_details() + { + int apiLevel = Build.VERSION.SDK_INT; + String[] array = WALLETS_ON_GANACHE.get(String.valueOf(apiLevel)); + if (array == null) + { + fail("Please config seed phrase and wallet address for this API level first."); + } + + Credentials deployCredentials = Credentials.create(contractOwnerPk); + String ownerAddress = Keys.toChecksumAddress(deployCredentials.getAddress()); + + importPKWalletFromFrontPage(contractOwnerPk); + + assertThat(getWalletAddress(), equalTo(ownerAddress)); + + addNewNetwork("Ganache", GANACHE_URL); + selectTestNet(useMumbai ? "Mumbai" : "Ganache"); + + //Ensure we're on the wallet page + switchToWallet(ownerAddress); + + Helper.wait(1); + + //add the token manually since test doesn't seem to work normally + click(withId(R.id.action_my_wallet)); + click(withSubstring("Add / Hide Tokens")); + Helper.wait(1); + click(withId(R.id.action_add)); + Helper.wait(1); + + onView(allOf(withId(R.id.edit_text))).perform(replaceText(doorContractAddress)); + + onView(isRoot()).perform(waitUntil(withId(R.id.select_token), 300)); + + click(withId(R.id.select_token)); + + click(withSubstring("Save")); + + pressBack(); + + //Swipe up + onView(withId(R.id.coordinator)).perform(ViewActions.swipeUp()); + + //Select token + click(withSubstring("OFFIC"), 120); + + //Wait for cert to resolve + //click certificate + click(withId(R.id.image_lock), 180); + + shouldSee("Smart Token Labs"); + shouldSee("ECDSA"); + shouldSee("Contract Owner"); // Note this may fail once we pull owner() from contract, + // test will need to be changed to contract owner, + // which for this test is: 0xA20efc4B9537d27acfD052003e311f762620642D + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/TransferERC20Test.java b/app/src/androidTest/java/com/alphawallet/app/TransferERC20Test.java new file mode 100644 index 0000000000..54b9beec9c --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/TransferERC20Test.java @@ -0,0 +1,102 @@ +package com.alphawallet.app; + +import static com.alphawallet.app.steps.Steps.GANACHE_URL; +import static com.alphawallet.app.steps.Steps.addCustomToken; +import static com.alphawallet.app.steps.Steps.addNewNetwork; +import static com.alphawallet.app.steps.Steps.assertBalanceIs; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.ensureTransactionConfirmed; +import static com.alphawallet.app.steps.Steps.getWalletAddress; +import static com.alphawallet.app.steps.Steps.gotoWalletPage; +import static com.alphawallet.app.steps.Steps.importWalletFromSettingsPage; +import static com.alphawallet.app.steps.Steps.selectTestNet; +import static com.alphawallet.app.steps.Steps.sendBalanceTo; +import static com.alphawallet.app.steps.Steps.switchToWallet; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.os.Build; + +import com.alphawallet.app.resources.Contracts; +import com.alphawallet.app.util.EthUtils; + +import org.junit.Before; +import org.junit.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +public class TransferERC20Test extends BaseE2ETest +{ + private String contractAddress; + private final String contractOwnerPk = "0x69c22d654be7fe75e31fbe26cb56c93ec91144fab67cb71529c8081971635069"; + // On CI server, run tests on different API levels concurrently may cause failure: Replacement transaction underpriced. + // Use different wallet to transfer token from can avoid this error + private static final Map WALLETS_ON_GANACHE = new HashMap() + { + { + put("24", new String[]{"0x644022aef70ad515ee186345fd74b005d759f41be8157c2835de3597d943146d", "0xE494323823fdF1A1Ab6ca79d2538C7182690D52a"}); + put("30", new String[]{"0x5c8843768e0e1916255def80ae7f6197e1f6a2dbcba720038748fc7634e5cffd", "0x162f5e0b63646AAA33a85eA13346F15C5289f901"}); + put("32", new String[]{"0x992b442eaa34de3c6ba0b61c75b2e4e0241d865443e313c4fa6ab8ba488a6957", "0xd7Ba01f596a7cc926b96b3B0a037c47A22904c06"}); + } + }; + private Web3j web3j; + private String senderPrivateKey; + private Credentials senderCredentials; + private Credentials contractOwnerCredentials; + + @Override + @Before + public void setUp() + { + int apiLevel = Build.VERSION.SDK_INT; + String[] array = WALLETS_ON_GANACHE.get(String.valueOf(apiLevel)); + if (array == null) + { + fail("Please config seed phrase and wallet address for this API level first."); + } + + senderPrivateKey = array[0]; + senderCredentials = Credentials.create(senderPrivateKey); + contractOwnerCredentials = Credentials.create(contractOwnerPk); + + super.setUp(); + web3j = EthUtils.buildWeb3j(GANACHE_URL); + deployTestTokenOnGanache(); + } + + private void deployTestTokenOnGanache() + { + //Transfer 1 eth into deployment wallet + EthUtils.transferFunds(web3j, senderCredentials, contractOwnerCredentials.getAddress(), BigDecimal.ONE); + + //Deploy door contract + EthUtils.deployContract(web3j, contractOwnerCredentials, Contracts.erc20ContractCode); + + //Always use zero nonce for determining the contract address + contractAddress = EthUtils.calculateContractAddress(contractOwnerCredentials.getAddress(), 0L); + + assertNotNull(contractAddress); + } + + @Test + public void should_transfer_from_an_account_to_another() + { + createNewWallet(); + String newWalletAddress = getWalletAddress(); + + importWalletFromSettingsPage(contractOwnerPk); + addNewNetwork("Ganache", GANACHE_URL); + selectTestNet("Ganache"); + gotoWalletPage(); + addCustomToken(contractAddress); + sendBalanceTo("AW test token", "1.11", newWalletAddress); + ensureTransactionConfirmed(); + switchToWallet(newWalletAddress); + addCustomToken(contractAddress); + assertBalanceIs("1.11"); + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/TransferTest.java b/app/src/androidTest/java/com/alphawallet/app/TransferTest.java index fd72e69fd4..3205f4fee1 100644 --- a/app/src/androidTest/java/com/alphawallet/app/TransferTest.java +++ b/app/src/androidTest/java/com/alphawallet/app/TransferTest.java @@ -1,5 +1,7 @@ package com.alphawallet.app; +import static com.alphawallet.app.steps.Steps.GANACHE_URL; +import static com.alphawallet.app.steps.Steps.addNewNetwork; import static com.alphawallet.app.steps.Steps.assertBalanceIs; import static com.alphawallet.app.steps.Steps.createNewWallet; import static com.alphawallet.app.steps.Steps.ensureTransactionConfirmed; @@ -8,50 +10,49 @@ import static com.alphawallet.app.steps.Steps.selectTestNet; import static com.alphawallet.app.steps.Steps.sendBalanceTo; import static com.alphawallet.app.steps.Steps.switchToWallet; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; import android.os.Build; -import org.junit.Ignore; import org.junit.Test; import java.util.HashMap; import java.util.Map; -public class TransferTest extends BaseE2ETest { +public class TransferTest extends BaseE2ETest +{ // On CI server, run tests on different API levels concurrently may cause failure: Replacement transaction underpriced. // Use different wallet to transfer token from can avoid this error - private static final Map WALLETS = new HashMap() {{ - put("24", new String[]{"essence allow crisp figure tired task melt honey reduce planet twenty rookie", "0xD0c424B3016E9451109ED97221304DeC639b3F84"}); - put("31", new String[]{"deputy review citizen bacon measure combine bag dose chronic retreat attack fly", "0xD8790c1eA5D15F8149C97F80524AC87f56301204"}); - put("32", new String[]{"omit mobile upgrade warm flock two era hamster local cat wink virus", "0x32f6F38137a79EA8eA237718b0AFAcbB1c58ca2e"}); - }}; + private static final Map WALLETS_ON_GANACHE = new HashMap() + { + { + put("24", new String[]{"0x644022aef70ad515ee186345fd74b005d759f41be8157c2835de3597d943146d", "0xE494323823fdF1A1Ab6ca79d2538C7182690D52a"}); + put("30", new String[]{"0x5c8843768e0e1916255def80ae7f6197e1f6a2dbcba720038748fc7634e5cffd", "0x162f5e0b63646AAA33a85eA13346F15C5289f901"}); + put("32", new String[]{"0x992b442eaa34de3c6ba0b61c75b2e4e0241d865443e313c4fa6ab8ba488a6957", "0xd7Ba01f596a7cc926b96b3B0a037c47A22904c06"}); + } + }; @Test - @Ignore - public void should_transfer_from_an_account_to_another() { + public void should_transfer_from_an_account_to_another() + { int apiLevel = Build.VERSION.SDK_INT; - String[] array = WALLETS.get(String.valueOf(apiLevel)); - if (array == null) { + String[] array = WALLETS_ON_GANACHE.get(String.valueOf(apiLevel)); + if (array == null) + { fail("Please config seed phrase and wallet address for this API level first."); } - String seedPhrase = array[0]; - String existedWalletAddress = array[1]; + String privateKey = array[0]; createNewWallet(); String newWalletAddress = getWalletAddress(); - importWalletFromSettingsPage(seedPhrase); - assertThat(getWalletAddress(), equalTo(existedWalletAddress)); - - selectTestNet(); - sendBalanceTo(newWalletAddress, "0.001"); + importWalletFromSettingsPage(privateKey); + addNewNetwork("Ganache", GANACHE_URL); + selectTestNet("Ganache"); + sendBalanceTo("ETH", "0.001", newWalletAddress); ensureTransactionConfirmed(); switchToWallet(newWalletAddress); assertBalanceIs("0.001"); } - } diff --git a/app/src/androidTest/java/com/alphawallet/app/WalletNameTest.java b/app/src/androidTest/java/com/alphawallet/app/WalletNameTest.java new file mode 100644 index 0000000000..55e25f4f57 --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/WalletNameTest.java @@ -0,0 +1,68 @@ +package com.alphawallet.app; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldSee; +import static com.alphawallet.app.steps.Steps.createNewWallet; +import static com.alphawallet.app.steps.Steps.getWalletAddress; +import static com.alphawallet.app.steps.Steps.gotoWalletPage; +import static com.alphawallet.app.steps.Steps.input; +import static com.alphawallet.app.steps.Steps.watchWalletWithENS; +import static com.alphawallet.app.util.Helper.click; + +import com.alphawallet.app.util.Helper; + +import org.junit.Test; + +public class WalletNameTest extends BaseE2ETest +{ + @Test + public void should_show_custom_name_instead_of_address() + { + createNewWallet(); + String address = getWalletAddress(); + + gotoWalletPage(); + shouldSeeFormattedAddress(address); + + renameWalletTo("TestWallet"); + shouldSee("TestWallet"); + + renameWalletTo(""); + shouldSeeFormattedAddress(address); + } + + @Test + public void should_show_custom_name_instead_of_ENS_name() + { + watchWalletWithENS("vitalik.eth"); + // Should see ENS name instead of address + shouldSee("vitalik.eth"); + + renameWalletTo("Vitalik"); + gotoWalletPage(); + shouldSee("Vitalik"); + + renameWalletTo(""); + gotoWalletPage(); + shouldSee("vitalik.eth"); + } + + private void renameWalletTo(String name) + { + click(withId(R.id.action_my_wallet)); + click(withSubstring("Rename this Wallet")); + onView(withId(R.id.edit_text)).perform(replaceText(name)); + input(R.id.input_name, name); + click(withText("Save Name")); + Helper.wait(2); + } + + private void shouldSeeFormattedAddress(String address) + { + shouldSee(address.substring(0, 6) + "..." + address.substring(address.length() - 4)); // 0xabcd...wxyz + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/assertions/Should.java b/app/src/androidTest/java/com/alphawallet/app/assertions/Should.java index 13cce63446..643175c8e9 100644 --- a/app/src/androidTest/java/com/alphawallet/app/assertions/Should.java +++ b/app/src/androidTest/java/com/alphawallet/app/assertions/Should.java @@ -1,14 +1,42 @@ package com.alphawallet.app.assertions; import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withParent; import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; +import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.alphawallet.app.util.Helper.waitUntil; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import android.widget.TextView; + +import com.alphawallet.app.R; public class Should { + private static final int TIMEOUT_IN_SECONDS = 5 * 60; + public static void shouldSee(String text) { - onView(isRoot()).perform(waitUntil(withSubstring(text), 10 * 60)); + onView(isRoot()).perform(waitUntil(withSubstring(text), TIMEOUT_IN_SECONDS)); + } + + public static void shouldNotSee(String text) + { + onView(isRoot()).perform(waitUntil(not(withSubstring(text)), TIMEOUT_IN_SECONDS)); + } + + public static void shouldNotSee(int id) + { + onView(isRoot()).perform(waitUntil(not(withId(id)), TIMEOUT_IN_SECONDS)); + } + + public static void shouldSee(int id) + { + onView(isRoot()).perform(waitUntil(withId(id), 10 * 60)); } } diff --git a/app/src/androidTest/java/com/alphawallet/app/resources/Contracts.java b/app/src/androidTest/java/com/alphawallet/app/resources/Contracts.java new file mode 100644 index 0000000000..5dcfe4ab9d --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/resources/Contracts.java @@ -0,0 +1,10 @@ +package com.alphawallet.app.resources; + +/** + * Created by JB on 4/09/2022. + */ +public abstract class Contracts +{ + public static String doorContractCode = "0x60806040523480156200001157600080fd5b506040518060400160405280600b81526020016a29aa26102428902237b7b960a91b815250604051806040016040528060068152602001654f464649434560d01b81525081600090816200006691906200042d565b5060016200007582826200042d565b505050620000926200008c620000e460201b60201c565b620000e8565b620000a960076200013a60201b62000e501760201c565b6040518060600160405280603581526020016200218360359139600a90620000d290826200042d565b50620000dd62000143565b5062000521565b3390565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b80546001019055565b6006546000906001600160a01b03163314620001a65760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064015b60405180910390fd5b620001bd60076200023860201b62000e591760201c565b90506127108110620002125760405162461bcd60e51b815260206004820152601460248201527f486974207570706572206d696e74206c696d697400000000000000000000000060448201526064016200019d565b6200021e33826200023c565b6200023560076200013a60201b62000e501760201c565b90565b5490565b6001600160a01b038216620002945760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f206164647265737360448201526064016200019d565b6000818152600260205260409020546001600160a01b031615620002fb5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e7465640000000060448201526064016200019d565b6001600160a01b038216600090815260036020526040812080546001929062000326908490620004f9565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b03861690811790915590518392907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b505050565b634e487b7160e01b600052604160045260246000fd5b600181811c90821680620003b457607f821691505b602082108103620003d557634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200038457600081815260208120601f850160051c81016020861015620004045750805b601f850160051c820191505b81811015620004255782815560010162000410565b505050505050565b81516001600160401b0381111562000449576200044962000389565b62000461816200045a84546200039f565b84620003db565b602080601f831160018114620004995760008415620004805750858301515b600019600386901b1c1916600185901b17855562000425565b600085815260208120601f198616915b82811015620004ca57888601518255948401946001909101908401620004a9565b5085821015620004e95787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b808201808211156200051b57634e487b7160e01b600052601160045260246000fd5b92915050565b611c5280620005316000396000f3fe60806040526004361061019c5760003560e01c806384c4bd4b116100ec578063a740fc871161008a578063e67876fe11610064578063e67876fe14610452578063e8a3d48514610469578063e985e9c51461047e578063f2fde38b1461049e57600080fd5b8063a740fc87146103fb578063b88d4fde14610412578063c87b56dd1461043257600080fd5b8063985e49f4116100c6578063985e49f4146103a95780639cb8a26a146103be578063a22cb465146103c6578063a49ff5b2146103e657600080fd5b806384c4bd4b1461035f5780638da5cb5b1461037657806395d89b411461039457600080fd5b80634bb309121161015957806370a082311161013357806370a08231146102e7578063715018a6146103155780637b47ec1a1461032a57806382345f991461034a57600080fd5b80634bb30912146102925780636352211e146102a75780636f3bffd2146102c757600080fd5b806301ffc9a7146101a157806306fdde03146101d6578063081812fc146101f8578063095ea7b31461023057806323b872dd1461025257806342842e0e14610272575b600080fd5b3480156101ad57600080fd5b506101c16101bc366004611585565b6104be565b60405190151581526020015b60405180910390f35b3480156101e257600080fd5b506101eb610510565b6040516101cd91906115ef565b34801561020457600080fd5b50610218610213366004611602565b6105a2565b6040516001600160a01b0390911681526020016101cd565b34801561023c57600080fd5b5061025061024b366004611632565b61063c565b005b34801561025e57600080fd5b5061025061026d36600461165c565b610751565b34801561027e57600080fd5b5061025061028d36600461165c565b6107ac565b34801561029e57600080fd5b506101eb6107c7565b3480156102b357600080fd5b506102186102c2366004611602565b6107d6565b3480156102d357600080fd5b506102506102e2366004611724565b61084d565b3480156102f357600080fd5b5061030761030236600461176d565b6108be565b6040519081526020016101cd565b34801561032157600080fd5b50610250610945565b34801561033657600080fd5b50610250610345366004611602565b61097b565b34801561035657600080fd5b50610307610a15565b34801561036b57600080fd5b506007546103079081565b34801561038257600080fd5b506006546001600160a01b0316610218565b3480156103a057600080fd5b506101eb610ac7565b3480156103b557600080fd5b50610307610ad6565b610250610b67565b3480156103d257600080fd5b506102506103e1366004611788565b610b9f565b3480156103f257600080fd5b50610307610bae565b34801561040757600080fd5b506009546103079081565b34801561041e57600080fd5b5061025061042d3660046117c4565b610c0f565b34801561043e57600080fd5b506101eb61044d366004611602565b610c71565b34801561045e57600080fd5b506008546103079081565b34801561047557600080fd5b506101eb610d6a565b34801561048a57600080fd5b506101c1610499366004611840565b610d8a565b3480156104aa57600080fd5b506102506104b936600461176d565b610db8565b60006001600160e01b031982166380ac58cd60e01b14806104ef57506001600160e01b03198216635b5e139f60e01b145b8061050a57506301ffc9a760e01b6001600160e01b03198316145b92915050565b60606000805461051f90611873565b80601f016020809104026020016040519081016040528092919081815260200182805461054b90611873565b80156105985780601f1061056d57610100808354040283529160200191610598565b820191906000526020600020905b81548152906001019060200180831161057b57829003601f168201915b5050505050905090565b6000818152600260205260408120546001600160a01b03166106205760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a20617070726f76656420717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b60648201526084015b60405180910390fd5b506000908152600460205260409020546001600160a01b031690565b6000610647826107d6565b9050806001600160a01b0316836001600160a01b0316036106b45760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b6064820152608401610617565b336001600160a01b03821614806106d057506106d08133610d8a565b6107425760405162461bcd60e51b815260206004820152603860248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f74206f7760448201527f6e6572206e6f7220617070726f76656420666f7220616c6c00000000000000006064820152608401610617565b61074c8383610e5d565b505050565b6006546001600160a01b0316331461077b5760405162461bcd60e51b8152600401610617906118ad565b6107853382610ecb565b6107a15760405162461bcd60e51b8152600401610617906118e2565b61074c838383610fa2565b61074c83838360405180602001604052806000815250610c0f565b6060600a805461051f90611873565b6000818152600260205260408120546001600160a01b03168061050a5760405162461bcd60e51b815260206004820152602960248201527f4552433732313a206f776e657220717565727920666f72206e6f6e657869737460448201526832b73a103a37b5b2b760b91b6064820152608401610617565b6006546001600160a01b031633146108775760405162461bcd60e51b8152600401610617906118ad565b600a6108838282611981565b507fd6666840ba3b0939cf78131cb173315c425a3385a30b8921494500ca2b49f34a816040516108b391906115ef565b60405180910390a150565b60006001600160a01b0382166109295760405162461bcd60e51b815260206004820152602a60248201527f4552433732313a2062616c616e636520717565727920666f7220746865207a65604482015269726f206164647265737360b01b6064820152608401610617565b506001600160a01b031660009081526003602052604090205490565b6006546001600160a01b0316331461096f5760405162461bcd60e51b8152600401610617906118ad565b610979600061113e565b565b6006546001600160a01b031633146109a55760405162461bcd60e51b8152600401610617906118ad565b6000818152600260205260409020546001600160a01b0316610a095760405162461bcd60e51b815260206004820152601760248201527f6275726e3a206e6f6e6578697374656e7420746f6b656e0000000000000000006044820152606401610617565b610a1281611190565b50565b6006546000906001600160a01b03163314610a425760405162461bcd60e51b8152600401610617906118ad565b612710610a4e60085490565b610a589190611a57565b9050610a676127106002611a6a565b8110610aac5760405162461bcd60e51b8152602060048201526014602482015273121a5d081d5c1c195c881b5a5b9d081b1a5b5a5d60621b6044820152606401610617565b610ab6338261122b565b610ac4600880546001019055565b90565b60606001805461051f90611873565b6006546000906001600160a01b03163314610b035760405162461bcd60e51b8152600401610617906118ad565b506007546127108110610b4f5760405162461bcd60e51b8152602060048201526014602482015273121a5d081d5c1c195c881b5a5b9d081b1a5b5a5d60621b6044820152606401610617565b610b59338261122b565b610ac4600780546001019055565b6006546001600160a01b03163314610b915760405162461bcd60e51b8152600401610617906118ad565b6006546001600160a01b0316ff5b610baa33838361136d565b5050565b6006546000906001600160a01b03163314610bdb5760405162461bcd60e51b8152600401610617906118ad565b610be86127106002611a6a565b600954610bf59190611a57565b9050610c01338261122b565b610ac4600980546001019055565b6006546001600160a01b03163314610c395760405162461bcd60e51b8152600401610617906118ad565b610c433383610ecb565b610c5f5760405162461bcd60e51b8152600401610617906118e2565b610c6b8484848461143b565b50505050565b6000818152600260205260409020546060906001600160a01b0316610cea5760405162461bcd60e51b815260206004820152602960248201527f746f6b656e5552493a2055524920717565727920666f72206e6f6e657869737460448201526832b73a103a37b5b2b760b91b6064820152608401610617565b612710821015610d1357604051806060016040528060358152602001611b496035913992915050565b610d206127106002611a6a565b821015610d4657604051806060016040528060358152602001611b7e6035913992915050565b604051806060016040528060358152602001611bb36035913992915050565b919050565b6060604051806060016040528060358152602001611be860359139905090565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b6006546001600160a01b03163314610de25760405162461bcd60e51b8152600401610617906118ad565b6001600160a01b038116610e475760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b6064820152608401610617565b610a128161113e565b80546001019055565b5490565b600081815260046020526040902080546001600160a01b0319166001600160a01b0384169081179091558190610e92826107d6565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b6000818152600260205260408120546001600160a01b0316610f445760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a206f70657261746f7220717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b6064820152608401610617565b6000610f4f836107d6565b9050806001600160a01b0316846001600160a01b03161480610f765750610f768185610d8a565b80610f9a5750836001600160a01b0316610f8f846105a2565b6001600160a01b0316145b949350505050565b826001600160a01b0316610fb5826107d6565b6001600160a01b0316146110195760405162461bcd60e51b815260206004820152602560248201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060448201526437bbb732b960d91b6064820152608401610617565b6001600160a01b03821661107b5760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b6064820152608401610617565b611086600082610e5d565b6001600160a01b03831660009081526003602052604081208054600192906110af908490611a89565b90915550506001600160a01b03821660009081526003602052604081208054600192906110dd908490611a57565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b0386811691821790925591518493918716917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b600061119b826107d6565b90506111a8600083610e5d565b6001600160a01b03811660009081526003602052604081208054600192906111d1908490611a89565b909155505060008281526002602052604080822080546001600160a01b0319169055518391906001600160a01b038416907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908390a45050565b6001600160a01b0382166112815760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f20616464726573736044820152606401610617565b6000818152600260205260409020546001600160a01b0316156112e65760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152606401610617565b6001600160a01b038216600090815260036020526040812080546001929061130f908490611a57565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b03861690811790915590518392907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b816001600160a01b0316836001600160a01b0316036113ce5760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c6572000000000000006044820152606401610617565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b611446848484610fa2565b6114528484848461146e565b610c6b5760405162461bcd60e51b815260040161061790611a9c565b60006001600160a01b0384163b1561156457604051630a85bd0160e11b81526001600160a01b0385169063150b7a02906114b2903390899088908890600401611aee565b6020604051808303816000875af19250505080156114ed575060408051601f3d908101601f191682019092526114ea91810190611b2b565b60015b61154a573d80801561151b576040519150601f19603f3d011682016040523d82523d6000602084013e611520565b606091505b5080516000036115425760405162461bcd60e51b815260040161061790611a9c565b805181602001fd5b6001600160e01b031916630a85bd0160e11b149050610f9a565b506001949350505050565b6001600160e01b031981168114610a1257600080fd5b60006020828403121561159757600080fd5b81356115a28161156f565b9392505050565b6000815180845260005b818110156115cf576020818501810151868301820152016115b3565b506000602082860101526020601f19601f83011685010191505092915050565b6020815260006115a260208301846115a9565b60006020828403121561161457600080fd5b5035919050565b80356001600160a01b0381168114610d6557600080fd5b6000806040838503121561164557600080fd5b61164e8361161b565b946020939093013593505050565b60008060006060848603121561167157600080fd5b61167a8461161b565b92506116886020850161161b565b9150604084013590509250925092565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff808411156116c9576116c9611698565b604051601f8501601f19908116603f011681019082821181831017156116f1576116f1611698565b8160405280935085815286868601111561170a57600080fd5b858560208301376000602087830101525050509392505050565b60006020828403121561173657600080fd5b813567ffffffffffffffff81111561174d57600080fd5b8201601f8101841361175e57600080fd5b610f9a848235602084016116ae565b60006020828403121561177f57600080fd5b6115a28261161b565b6000806040838503121561179b57600080fd5b6117a48361161b565b9150602083013580151581146117b957600080fd5b809150509250929050565b600080600080608085870312156117da57600080fd5b6117e38561161b565b93506117f16020860161161b565b925060408501359150606085013567ffffffffffffffff81111561181457600080fd5b8501601f8101871361182557600080fd5b611834878235602084016116ae565b91505092959194509250565b6000806040838503121561185357600080fd5b61185c8361161b565b915061186a6020840161161b565b90509250929050565b600181811c9082168061188757607f821691505b6020821081036118a757634e487b7160e01b600052602260045260246000fd5b50919050565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526031908201527f4552433732313a207472616e736665722063616c6c6572206973206e6f74206f6040820152701ddb995c881b9bdc88185c1c1c9bdd9959607a1b606082015260800190565b601f82111561074c57600081815260208120601f850160051c8101602086101561195a5750805b601f850160051c820191505b8181101561197957828155600101611966565b505050505050565b815167ffffffffffffffff81111561199b5761199b611698565b6119af816119a98454611873565b84611933565b602080601f8311600181146119e457600084156119cc5750858301515b600019600386901b1c1916600185901b178555611979565b600085815260208120601f198616915b82811015611a13578886015182559484019460019091019084016119f4565b5085821015611a315787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b634e487b7160e01b600052601160045260246000fd5b8082018082111561050a5761050a611a41565b6000816000190483118215151615611a8457611a84611a41565b500290565b8181038181111561050a5761050a611a41565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b6001600160a01b0385811682528416602082015260408101839052608060608201819052600090611b21908301846115a9565b9695505050505050565b600060208284031215611b3d57600080fd5b81516115a28161156f56fe697066733a2f2f516d57393438614e34546a6834654c6b41416f386f733141634d32464a6a413436717461456646416e794e597a59697066733a2f2f516d523331663241556f6b433551794c587a4459556a7935745669626b6a625734766f56754d425a66724e565538697066733a2f2f516d646153546146365758705957694c35636b3763736d5479354557487a595647796b4a5a4e3754523935645353697066733a2f2f516d5567644c7650766a754847664d73754b3148326a467067357231514e63384a655779587952774b5038705466a2646970667358221220e2ef3122a6cb2d5c73f6e9c50a0261a53c008746506d8db79d5ea5e188d01d8a64736f6c63430008100033697066733a2f2f516d58584c464265536a5841774168626f31333434774a536a4c676f557266554b394c4535376f56756261525270"; + public static String erc20ContractCode = "0x60806040523480156200001157600080fd5b506040518060400160405280600d81526020017f4157207465737420746f6b656e000000000000000000000000000000000000008152506040518060400160405280600481526020017f415754540000000000000000000000000000000000000000000000000000000081525081600390816200008f9190620004e3565b508060049081620000a19190620004e3565b505050620000e333620000b9620000e960201b60201c565b600a620000c791906200075a565b62989680620000d79190620007ab565b620000f260201b60201c565b620008e2565b60006012905090565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160362000164576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016200015b9062000857565b60405180910390fd5b62000178600083836200025f60201b60201c565b80600260008282546200018c919062000879565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516200023f9190620008c5565b60405180910390a36200025b600083836200026460201b60201c565b5050565b505050565b505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680620002eb57607f821691505b602082108103620003015762000300620002a3565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026200036b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826200032c565b6200037786836200032c565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b6000620003c4620003be620003b8846200038f565b62000399565b6200038f565b9050919050565b6000819050919050565b620003e083620003a3565b620003f8620003ef82620003cb565b84845462000339565b825550505050565b600090565b6200040f62000400565b6200041c818484620003d5565b505050565b5b8181101562000444576200043860008262000405565b60018101905062000422565b5050565b601f82111562000493576200045d8162000307565b62000468846200031c565b8101602085101562000478578190505b6200049062000487856200031c565b83018262000421565b50505b505050565b600082821c905092915050565b6000620004b86000198460080262000498565b1980831691505092915050565b6000620004d38383620004a5565b9150826002028217905092915050565b620004ee8262000269565b67ffffffffffffffff8111156200050a576200050962000274565b5b620005168254620002d2565b6200052382828562000448565b600060209050601f8311600181146200055b576000841562000546578287015190505b620005528582620004c5565b865550620005c2565b601f1984166200056b8662000307565b60005b8281101562000595578489015182556001820191506020850194506020810190506200056e565b86831015620005b55784890151620005b1601f891682620004a5565b8355505b6001600288020188555050505b505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008160011c9050919050565b6000808291508390505b6001851115620006585780860481111562000630576200062f620005ca565b5b6001851615620006405780820291505b80810290506200065085620005f9565b945062000610565b94509492505050565b60008262000673576001905062000746565b8162000683576000905062000746565b81600181146200069c5760028114620006a757620006dd565b600191505062000746565b60ff841115620006bc57620006bb620005ca565b5b8360020a915084821115620006d657620006d5620005ca565b5b5062000746565b5060208310610133831016604e8410600b8410161715620007175782820a905083811115620007115762000710620005ca565b5b62000746565b62000726848484600162000606565b9250905081840481111562000740576200073f620005ca565b5b81810290505b9392505050565b600060ff82169050919050565b600062000767826200038f565b915062000774836200074d565b9250620007a37fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff848462000661565b905092915050565b6000620007b8826200038f565b9150620007c5836200038f565b9250828202620007d5816200038f565b91508282048414831517620007ef57620007ee620005ca565b5b5092915050565b600082825260208201905092915050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b60006200083f601f83620007f6565b91506200084c8262000807565b602082019050919050565b60006020820190508181036000830152620008728162000830565b9050919050565b600062000886826200038f565b915062000893836200038f565b9250828201905080821115620008ae57620008ad620005ca565b5b92915050565b620008bf816200038f565b82525050565b6000602082019050620008dc6000830184620008b4565b92915050565b61122f80620008f26000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b0c565b60405180910390f35b6100e660048036038101906100e19190610bc7565b610308565b6040516100f39190610c22565b60405180910390f35b61010461032b565b6040516101119190610c4c565b60405180910390f35b610134600480360381019061012f9190610c67565b610335565b6040516101419190610c22565b60405180910390f35b610152610364565b60405161015f9190610cd6565b60405180910390f35b610182600480360381019061017d9190610bc7565b61036d565b60405161018f9190610c22565b60405180910390f35b6101b260048036038101906101ad9190610cf1565b6103a4565b6040516101bf9190610c4c565b60405180910390f35b6101d06103ec565b6040516101dd9190610b0c565b60405180910390f35b61020060048036038101906101fb9190610bc7565b61047e565b60405161020d9190610c22565b60405180910390f35b610230600480360381019061022b9190610bc7565b6104f5565b60405161023d9190610c22565b60405180910390f35b610260600480360381019061025b9190610d1e565b610518565b60405161026d9190610c4c565b60405180910390f35b60606003805461028590610d8d565b80601f01602080910402602001604051908101604052809291908181526020018280546102b190610d8d565b80156102fe5780601f106102d3576101008083540402835291602001916102fe565b820191906000526020600020905b8154815290600101906020018083116102e157829003601f168201915b5050505050905090565b60008061031361059f565b90506103208185856105a7565b600191505092915050565b6000600254905090565b60008061034061059f565b905061034d858285610770565b6103588585856107fc565b60019150509392505050565b60006012905090565b60008061037861059f565b905061039981858561038a8589610518565b6103949190610ded565b6105a7565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546103fb90610d8d565b80601f016020809104026020016040519081016040528092919081815260200182805461042790610d8d565b80156104745780601f1061044957610100808354040283529160200191610474565b820191906000526020600020905b81548152906001019060200180831161045757829003601f168201915b5050505050905090565b60008061048961059f565b905060006104978286610518565b9050838110156104dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104d390610e93565b60405180910390fd5b6104e982868684036105a7565b60019250505092915050565b60008061050061059f565b905061050d8185856107fc565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610616576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161060d90610f25565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610685576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161067c90610fb7565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516107639190610c4c565b60405180910390a3505050565b600061077c8484610518565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146107f657818110156107e8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107df90611023565b60405180910390fd5b6107f584848484036105a7565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361086b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610862906110b5565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108da576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108d190611147565b60405180910390fd5b6108e5838383610a72565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490508181101561096b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610962906111d9565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610a599190610c4c565b60405180910390a3610a6c848484610a77565b50505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610ab6578082015181840152602081019050610a9b565b60008484015250505050565b6000601f19601f8301169050919050565b6000610ade82610a7c565b610ae88185610a87565b9350610af8818560208601610a98565b610b0181610ac2565b840191505092915050565b60006020820190508181036000830152610b268184610ad3565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610b5e82610b33565b9050919050565b610b6e81610b53565b8114610b7957600080fd5b50565b600081359050610b8b81610b65565b92915050565b6000819050919050565b610ba481610b91565b8114610baf57600080fd5b50565b600081359050610bc181610b9b565b92915050565b60008060408385031215610bde57610bdd610b2e565b5b6000610bec85828601610b7c565b9250506020610bfd85828601610bb2565b9150509250929050565b60008115159050919050565b610c1c81610c07565b82525050565b6000602082019050610c376000830184610c13565b92915050565b610c4681610b91565b82525050565b6000602082019050610c616000830184610c3d565b92915050565b600080600060608486031215610c8057610c7f610b2e565b5b6000610c8e86828701610b7c565b9350506020610c9f86828701610b7c565b9250506040610cb086828701610bb2565b9150509250925092565b600060ff82169050919050565b610cd081610cba565b82525050565b6000602082019050610ceb6000830184610cc7565b92915050565b600060208284031215610d0757610d06610b2e565b5b6000610d1584828501610b7c565b91505092915050565b60008060408385031215610d3557610d34610b2e565b5b6000610d4385828601610b7c565b9250506020610d5485828601610b7c565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610da557607f821691505b602082108103610db857610db7610d5e565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610df882610b91565b9150610e0383610b91565b9250828201905080821115610e1b57610e1a610dbe565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610e7d602583610a87565b9150610e8882610e21565b604082019050919050565b60006020820190508181036000830152610eac81610e70565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000610f0f602483610a87565b9150610f1a82610eb3565b604082019050919050565b60006020820190508181036000830152610f3e81610f02565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000610fa1602283610a87565b9150610fac82610f45565b604082019050919050565b60006020820190508181036000830152610fd081610f94565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b600061100d601d83610a87565b915061101882610fd7565b602082019050919050565b6000602082019050818103600083015261103c81611000565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b600061109f602583610a87565b91506110aa82611043565b604082019050919050565b600060208201905081810360008301526110ce81611092565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000611131602383610a87565b915061113c826110d5565b604082019050919050565b6000602082019050818103600083015261116081611124565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b60006111c3602683610a87565b91506111ce82611167565b604082019050919050565b600060208201905081810360008301526111f2816111b6565b905091905056fea26469706673582212206637373f9ebc605cdad9359cb179c62d11a616e2ad65618a36fc5bb768793f6364736f6c63430008110033"; +} diff --git a/app/src/androidTest/java/com/alphawallet/app/steps/Steps.java b/app/src/androidTest/java/com/alphawallet/app/steps/Steps.java index bffb5435c9..077b2ccbda 100644 --- a/app/src/androidTest/java/com/alphawallet/app/steps/Steps.java +++ b/app/src/androidTest/java/com/alphawallet/app/steps/Steps.java @@ -1,11 +1,12 @@ package com.alphawallet.app.steps; import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; import static androidx.test.espresso.Espresso.pressBack; import static androidx.test.espresso.action.ViewActions.pressImeActionButton; import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.action.ViewActions.scrollTo; -import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition; +import static androidx.test.espresso.action.ViewActions.swipeUp; import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.withHint; @@ -13,14 +14,17 @@ import static androidx.test.espresso.matcher.ViewMatchers.withParent; import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.alphawallet.app.assertions.Should.shouldNotSee; +import static com.alphawallet.app.assertions.Should.shouldSee; import static com.alphawallet.app.util.Helper.click; +import static com.alphawallet.app.util.Helper.clickListItem; +import static com.alphawallet.app.util.Helper.waitForLoadingComplete; import static com.alphawallet.app.util.Helper.waitUntil; import static com.alphawallet.app.util.RootUtil.isDeviceRooted; - -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.core.StringStartsWith.startsWith; +import androidx.test.core.app.ApplicationProvider; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.action.ViewActions; @@ -29,20 +33,36 @@ import com.alphawallet.app.util.GetTextAction; import com.alphawallet.app.util.Helper; +import org.hamcrest.core.AllOf; + /** * Every step consists of several operations, step name stands for user perspective actions. * You can add steps as you wish to reuse code between test cases. */ public class Steps { + public static final String GANACHE_URL = "http://10.0.2.2:8545/"; + public static void createNewWallet() { - if (isDeviceRooted()) { + click(withId(R.id.button_create)); + closeSelectNetworkPage(); + click(withText(R.string.action_close)); + } + + public static void closeSecurityWarning() + { + if (isDeviceRooted()) + { click(withText(R.string.ok)); } - click(withId(R.id.button_create)); - Helper.wait(10); - click(withText(R.string.action_close)); // works well locally but NOT work with GitHub actions + } + + public static void closeSelectNetworkPage() + { + pressBack(); + Helper.wait(1); + shouldSee(R.id.nav_settings_text); } public static void visit(String urlString) @@ -57,53 +77,73 @@ public static void navigateToBrowser() click(withId(R.id.nav_browser_text)); } - public static void selectTestNet() + public static void selectTestNet(String name) { gotoSettingsPage(); selectMenu("Select Active Networks"); toggleSwitch(R.id.mainnet_header); click(withText(R.string.action_enable_testnet)); - onView(withId(R.id.test_list)).perform(actionOnItemAtPosition(1, ViewActions.click())); // Rinkeby - onView(withId(R.id.test_list)).perform(actionOnItemAtPosition(3, ViewActions.click())); // Kovan + Helper.wait(1); + clickListItem(R.id.test_list, withSubstring("Görli")); + clickListItem(R.id.test_list, withSubstring(name)); pressBack(); } - private static void selectMenu(String text) + public static void selectMenu(String text) { ViewInteraction selectActiveNetworks = onView(withText(text)); selectActiveNetworks.perform(scrollTo(), ViewActions.click()); } - public static void assertBalanceIs(String balanceStr) { + public static void assertBalanceIs(String balanceStr) + { Should.shouldSee(balanceStr); } - public static void ensureTransactionConfirmed() { + public static void ensureTransactionConfirmed() + { // onView(withText(R.string.rate_no_thanks)).perform(click()); click(withId(R.string.action_show_tx_details)); - onView(isRoot()).perform(waitUntil(withSubstring("Sent ETH"), 30 * 60)); + onView(isRoot()).perform(waitUntil(withSubstring("Send"), 30 * 60)); pressBack(); } - public static void sendBalanceTo(String receiverAddress, String amountStr) { + public static void sendBalanceTo(String tokenSymbol, String amountStr, String receiverAddress) + { click(withId(R.id.nav_wallet_text)); - onView(isRoot()).perform(waitUntil(R.id.eth_data, withText(not(startsWith("0"))))); - click(withId(R.id.eth_data)); + ensureBalanceFetched(); + click(withSubstring(tokenSymbol)); click(withText("Send")); onView(withHint("0")).perform(replaceText(amountStr)); onView(withHint(R.string.recipient_address)).perform(replaceText(receiverAddress)); click(withId(R.string.action_next)); - Helper.wait(5); - click(withId(R.string.action_confirm)); + try + { + click(withId(R.string.action_confirm)); + } + catch (Error | Exception e) + { + waitForLoadingComplete("Calculating Gas Limit"); + click(withId(R.string.action_confirm)); + } + } + + private static void ensureBalanceFetched() + { + shouldSee("Ganache"); + shouldNotSee("0 ETH"); } - public static void switchToWallet(String address) { + public static void switchToWallet(String address) + { gotoSettingsPage(); click(withText("Change / Add Wallet")); onView(withSubstring(address.substring(0, 6))).perform(ViewActions.click()); + waitUntil(withSubstring("Buy"), 30); } - public static String getWalletAddress() { + public static String getWalletAddress() + { gotoSettingsPage(); click(withText("Show My Wallet Address")); GetTextAction getTextAction = new GetTextAction(); @@ -112,43 +152,162 @@ public static String getWalletAddress() { return getTextAction.getText().toString().replace(" ", ""); // The address show on 2 lines so there is a blank space } - public static void importWalletFromSettingsPage(String seedPhrase) { + public static void importWalletFromSettingsPage(String text) + { gotoSettingsPage(); click(withText("Change / Add Wallet")); - Helper.wait(10); click(withId(R.id.action_add)); // SnapshotUtil.take("after-add"); click(withId(R.id.import_account_action)); - onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_seed)))))).perform(replaceText(seedPhrase)); + int textId; + int buttonId; + boolean isPrivateKey = text.startsWith("0x"); + if (isPrivateKey) + { + click(withText("Private key")); + textId = R.id.input_private_key; + buttonId = R.id.import_action_pk; + } + else + { + textId = R.id.input_seed; + buttonId = R.id.import_action; + } + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(textId)))))).perform(replaceText(text)); Helper.wait(2); // Avoid error: Error performing a ViewAction! soft keyboard dismissal animation may have been in the way. Retrying once after: 1000 millis - click(withId(R.id.import_action)); - Helper.wait(10); + click(withId(buttonId)); +// waitForLoadingComplete("Handling"); + Helper.wait(5); + closeSelectNetworkPage(); + } + + public static void importPKWalletFromFrontPage(String privateKey) + { + click(withText("I already have a Wallet")); + click(withText("Private key")); + Helper.wait(1); + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_private_key)))))).perform(replaceText(privateKey)); + Helper.wait(1); // Avoid error: Error performing a ViewAction! soft keyboard dismissal animation may have been in the way. Retrying once after: 1000 millis + click(withId(R.id.import_action_pk)); + Helper.wait(15); + } + + public static void importKSWalletFromFrontPage(String keystore, String password) + { + click(withText("I already have a Wallet")); + click(withText("Keystore")); + Helper.wait(1); + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_keystore)))))).perform(replaceText(keystore)); + Helper.wait(1); // Avoid error: Error performing a ViewAction! soft keyboard dismissal animation may have been in the way. Retrying once after: 1000 millis + click(withText("Continue")); + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_password)))))).perform(replaceText(password)); + click(withText("Continue")); + Helper.wait(15); + } + + public static void importKSWalletFromSettingsPage(String keystore, String password) + { + gotoSettingsPage(); + click(withText("Change / Add Wallet")); + click(withId(R.id.action_add)); + click(withId(R.id.import_account_action)); + click(withText("Keystore")); + Helper.wait(1); + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_keystore)))))).perform(replaceText(keystore)); + Helper.wait(1); // Avoid error: Error performing a ViewAction! soft keyboard dismissal animation may have been in the way. Retrying once after: 1000 millis + click(withText("Continue")); + onView(allOf(withId(R.id.edit_text), withParent(withParent(withParent(withId(R.id.input_password)))))).perform(replaceText(password)); + click(withText("Continue")); + Helper.wait(5); + shouldSee("Select Active Networks"); + pressBack(); } - public static void gotoSettingsPage() { + public static void gotoWalletPage() + { + click(withId(R.id.nav_wallet_text)); + } + + public static void gotoSettingsPage() + { click(withId(R.id.nav_settings_text)); } - private static void toggleSwitch(int id) { + public static void toggleSwitch(int id) + { onView(allOf(withId(R.id.switch_material), isDescendantOfA(withId(id)))).perform(ViewActions.click()); } - public static void addNewNetwork(String name) + public static void addNewNetwork(String name, String url) { + gotoSettingsPage(); selectMenu("Select Active Networks"); click(withId(R.id.action_add)); input(R.id.input_network_name, name); - input(R.id.input_network_rpc_url,"http://xxx.yyy.zzz"); - input(R.id.input_network_chain_id, "123456"); - input(R.id.input_network_symbol, "MTNSYM"); - input(R.id.input_network_explorer_api, "http://xxx.yyy.zzz"); - input(R.id.input_network_block_explorer_url, "http://xxx.yyy.zzz"); + input(R.id.input_network_rpc_url, url); + input(R.id.input_network_chain_id, "2"); + input(R.id.input_network_symbol, "ETH"); + input(R.id.input_network_explorer_api, url); + input(R.id.input_network_block_explorer_url, url); + onView(withId(R.id.network_input_scroll)).perform(swipeUp()); + Helper.wait(1); + click(withId(R.id.checkbox_testnet)); click(withId(R.string.action_add_network)); + pressBack(); } - private static void input(int id, String text) + public static void input(int id, String text) { onView(allOf(withId(R.id.edit_text), isDescendantOfA(withId(id)))).perform(replaceText(text)); } + public static void watchWalletWithENS(String ens) + { + click(withText("I already have a Wallet")); + click(withText("Private key")); // Scroll to right + Helper.wait(1); + click(withText("Watch-only Wallets")); + Helper.wait(1); + input(R.id.input_watch_address, ens); + Helper.wait(5); + click(withText("Watch Wallet")); + } + + public static void selectCurrency(String currency) + { + gotoSettingsPage(); + selectMenu("Change Currency"); + Helper.wait(1); + clickListItem(R.id.list, withText(currency)); + Helper.wait(1); + pressBack(); + } + + public static void openOptionsMenu() + { + openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext()); + } + + public static void addCustomToken(String contractAddress) + { + //add the token manually since test doesn't seem to work normally + click(withId(R.id.action_my_wallet)); + click(withSubstring("Add / Hide Tokens")); + Helper.wait(1); + click(withId(R.id.action_add)); + Helper.wait(1); + + onView(AllOf.allOf(withId(R.id.edit_text))).perform(replaceText(contractAddress)); + + onView(isRoot()).perform(waitUntil(withId(R.id.select_token), 300)); + + click(withId(R.id.select_token)); + + click(withSubstring("Save")); + + pressBack(); + + //Swipe up + onView(withId(R.id.coordinator)).perform(ViewActions.swipeUp()); + } } diff --git a/app/src/androidTest/java/com/alphawallet/app/util/EthUtils.java b/app/src/androidTest/java/com/alphawallet/app/util/EthUtils.java new file mode 100644 index 0000000000..02b1fae9d8 --- /dev/null +++ b/app/src/androidTest/java/com/alphawallet/app/util/EthUtils.java @@ -0,0 +1,122 @@ +package com.alphawallet.app.util; + +import com.alphawallet.app.C; +import com.alphawallet.app.service.AWHttpService; + +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Hash; +import org.web3j.crypto.Keys; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.response.EthGetTransactionCount; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.rlp.RlpEncoder; +import org.web3j.rlp.RlpList; +import org.web3j.rlp.RlpString; +import org.web3j.tx.Transfer; +import org.web3j.utils.Convert; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Single; +import okhttp3.OkHttpClient; + +/** + * Created by JB on 4/09/2022. + */ +public abstract class EthUtils +{ + public static Web3j buildWeb3j(String url) + { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(C.CONNECT_TIMEOUT, TimeUnit.SECONDS) + .connectTimeout(C.READ_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(C.WRITE_TIMEOUT, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + + AWHttpService svs = new AWHttpService(url, url, client, false); + return Web3j.build(svs); + } + + public static String calculateContractAddress(String account, long nonce) + { + byte[] addressAsBytes = org.web3j.utils.Numeric.hexStringToByteArray(account); + byte[] calculatedAddressAsBytes = + Hash.sha3(RlpEncoder.encode( + new RlpList( + RlpString.create(addressAsBytes), + RlpString.create((nonce))))); + + calculatedAddressAsBytes = Arrays.copyOfRange(calculatedAddressAsBytes, + 12, calculatedAddressAsBytes.length); + return Keys.toChecksumAddress(org.web3j.utils.Numeric.toHexString(calculatedAddressAsBytes)); + } + + public static Single getLastTransactionNonce(Web3j web3j, String walletAddress) + { + return Single.fromCallable(() -> { + try + { + EthGetTransactionCount ethGetTransactionCount = web3j + .ethGetTransactionCount(walletAddress, DefaultBlockParameterName.PENDING) + .send(); + return ethGetTransactionCount.getTransactionCount(); + } + catch (Exception e) + { + return BigInteger.ZERO; + } + }); + } + + public static void transferFunds(Web3j web3j, Credentials credentials, String targetAddr, BigDecimal ethAmount) + { + try + { + TransactionReceipt transactionReceipt = Transfer.sendFunds( + web3j, credentials, targetAddr, + ethAmount, Convert.Unit.ETHER).send(); + + System.out.println("TX: " + transactionReceipt.getTransactionHash()); + } + catch (Exception e) + { + // + } + } + + public static long deployContract(Web3j web3j, Credentials credentials, String contractCode) + { + long nonceReturn = 0; + try + { + BigInteger nonce = getLastTransactionNonce(web3j, credentials.getAddress()).blockingGet(); + + RawTransaction rawTransaction = RawTransaction.createContractTransaction(nonce, + BigInteger.valueOf(20000000000L), BigInteger.valueOf(6721975), BigInteger.ZERO, + contractCode); + + byte[] signedDeployTransaction = TransactionEncoder.signMessage(rawTransaction, credentials); + + EthSendTransaction raw = web3j + .ethSendRawTransaction(org.web3j.utils.Numeric.toHexString(signedDeployTransaction)).send(); + + System.out.println("Deploy hash: " + raw.getTransactionHash()); + + nonceReturn = nonce.longValue(); + } + catch (Exception e) + { + // + } + + return nonceReturn; + } +} diff --git a/app/src/androidTest/java/com/alphawallet/app/util/Helper.java b/app/src/androidTest/java/com/alphawallet/app/util/Helper.java index 50211d0d88..a7ea9d5f44 100644 --- a/app/src/androidTest/java/com/alphawallet/app/util/Helper.java +++ b/app/src/androidTest/java/com/alphawallet/app/util/Helper.java @@ -1,62 +1,89 @@ package com.alphawallet.app.util; -import android.view.View; +import static android.content.Context.INPUT_METHOD_SERVICE; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.RootMatchers.isDialog; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withSubstring; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.core.AllOf.allOf; -import org.hamcrest.Matcher; -import org.hamcrest.Matchers; +import android.content.Context; +import android.view.KeyEvent; +import android.view.View; -import java.util.concurrent.TimeoutException; +import android.view.inputmethod.InputMethodManager; import androidx.test.espresso.PerformException; import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; import androidx.test.espresso.action.ViewActions; +import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.espresso.util.HumanReadables; import androidx.test.espresso.util.TreeIterables; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static org.hamcrest.core.AllOf.allOf; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +import java.util.concurrent.TimeoutException; +import com.alphawallet.app.R; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; -public class Helper { - private static final int DEFAULT_TIMEOUT_IN_SECONDS = 10; +import java.util.concurrent.TimeoutException; + +public class Helper +{ + private static final int DEFAULT_TIMEOUT_IN_SECONDS = 30; - public static ViewAction waitUntil(final int viewId, final Matcher matcher) { + public static ViewAction waitUntil(final int viewId, final Matcher matcher) + { return waitUntil(allOf(withId(viewId), matcher), DEFAULT_TIMEOUT_IN_SECONDS); } - public static ViewAction waitUntil(final int viewId, final Matcher matcher, int timeoutInSeconds) { + public static ViewAction waitUntil(final int viewId, final Matcher matcher, int timeoutInSeconds) + { return waitUntil(allOf(withId(viewId), matcher), timeoutInSeconds); } - public static ViewAction waitUntil(Matcher matcher) { + public static ViewAction waitUntil(Matcher matcher) + { return waitUntil(matcher, DEFAULT_TIMEOUT_IN_SECONDS); } - public static ViewAction waitUntil(Matcher matcher, int timeoutInSeconds) { - return new ViewAction() { + public static ViewAction waitUntil(Matcher matcher, int timeoutInSeconds) + { + return new ViewAction() + { @Override - public Matcher getConstraints() { + public Matcher getConstraints() + { return isRoot(); } @Override - public String getDescription() { + public String getDescription() + { return "wait for view matches " + matcher.toString() + " during " + timeoutInSeconds + " seconds."; } @Override - public void perform(final UiController uiController, final View view) { + public void perform(final UiController uiController, final View view) + { uiController.loopMainThreadUntilIdle(); final long startTime = System.currentTimeMillis(); final long endTime = startTime + timeoutInSeconds * 1000L; - do { - - for (View child : TreeIterables.breadthFirstViewTraversal(view.getRootView())) { - if (matcher.matches(child)) { + do + { + for (View child : TreeIterables.breadthFirstViewTraversal(view.getRootView())) + { + if (matcher.matches(child)) + { return; } } @@ -75,47 +102,175 @@ public void perform(final UiController uiController, final View view) { }; } - public static void wait(int seconds) { - onView(isRoot()).perform(new ViewAction() { + public static void wait(int seconds) + { + onView(isRoot()).perform(new ViewAction() + { @Override - public Matcher getConstraints() { + public Matcher getConstraints() + { return isRoot(); } @Override - public String getDescription() { + public String getDescription() + { return "wait " + seconds + " seconds."; } @Override - public void perform(final UiController uiController, final View view) { + public void perform(final UiController uiController, final View view) + { uiController.loopMainThreadUntilIdle(); uiController.loopMainThreadForAtLeast(seconds * 1000L); } }); } - public static void click(Matcher matcher) { - Helper.wait(1); //slight pause + public static void click(Matcher matcher, int timeoutInSeconds) + { + onView(isRoot()).perform(Helper.waitUntil(Matchers.allOf(matcher, isDisplayed()), timeoutInSeconds)); + onView(matcher).perform(ViewActions.click(doNothing())); // if click executed as long press, do nothing and retry clicking + } + + public static void click(Matcher matcher) + { +// Helper.wait(1); //slight pause onView(isRoot()).perform(Helper.waitUntil(Matchers.allOf(matcher, isDisplayed()))); onView(matcher).perform(ViewActions.click(doNothing())); // if click executed as long press, do nothing and retry clicking } - private static ViewAction doNothing() { - return new ViewAction() { + private static ViewAction doNothing() + { + return new ViewAction() + { @Override - public Matcher getConstraints() { + public Matcher getConstraints() + { return isDisplayed(); } @Override - public String getDescription() { + public String getDescription() + { return "Do nothing."; } @Override - public void perform(UiController uiController, View view) { + public void perform(UiController uiController, View view) + { } }; } + + public static void clickListItem(int list, Matcher matcher) + { + for (int i = 0; i < 50; i++) + { + try + { + click(matcher, 0); + return; + } + catch (Exception e) + { + scrollDown(list); + } + } + throw new RuntimeException("Can not find " + matcher.toString()); + } + + private static void scrollDown(int list) + { + onView(withId(list)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_DPAD_DOWN)); + Helper.wait(1); + } + + public static void waitForLoadingComplete(String title) + { + waitUntilShown(title); + waitUntilDismissed(title); + } + + private static void waitUntilDismissed(String title) + { + while (true) + { + try + { + onView(withSubstring(title)).inRoot(isDialog()).check(matches(not(ViewMatchers.isDisplayed()))); + } + catch (Error e) + { + // Dialog still showing + wait(1); + } + catch (Exception e) + { + // Dialog dismissed + break; + } + } + } + + private static void waitUntilShown(String title) + { + while (true) + { + try + { + onView(withSubstring(title)).inRoot(isDialog()).check(matches(ViewMatchers.isDisplayed())); + break; + } + catch (Error | Exception e) + { + wait(1); + } + } + } + + public static boolean isSoftKeyboardShown(Context context) + { + InputMethodManager imm = + (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE); + return imm.isAcceptingText(); + } + + public static void waitUntilLoaded() + { + waitStart(); + waitComplete(); + } + + private static void waitComplete() + { + for (int i = 0; i < DEFAULT_TIMEOUT_IN_SECONDS; i++) + { + try + { + onView(withId(R.id.progressBar)).check(matches(not(isDisplayed()))); + break; + } + catch (Error | Exception e) + { + Helper.wait(1); + } + } + } + + private static void waitStart() + { + for (int i = 0; i < DEFAULT_TIMEOUT_IN_SECONDS; i++) + { + try + { + onView(withId(R.id.progressBar)).check(matches(isDisplayed())); + break; + } + catch (Error | Exception e) + { + Helper.wait(1); + } + } + } } diff --git a/app/src/androidTest/java/com/alphawallet/app/util/SnapshotUtil.java b/app/src/androidTest/java/com/alphawallet/app/util/SnapshotUtil.java index d02a1e8d23..bba09a6a4b 100644 --- a/app/src/androidTest/java/com/alphawallet/app/util/SnapshotUtil.java +++ b/app/src/androidTest/java/com/alphawallet/app/util/SnapshotUtil.java @@ -1,5 +1,6 @@ package com.alphawallet.app.util; +import android.os.Build; import android.os.Environment; import androidx.test.platform.app.InstrumentationRegistry; @@ -7,14 +8,26 @@ import java.io.File; -public class SnapshotUtil { - public static void take(String testName) { - File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()); - if (!path.exists()) { +import timber.log.Timber; + +public class SnapshotUtil +{ + public static String SNAPSHOT_DIR = ""; + + public static void take(String testName) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + { + Timber.tag("SnapshotUtil").d("Skipping snapshot for API < 30"); + return; + } + File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/" + SNAPSHOT_DIR); + if (!path.exists()) + { path.mkdirs(); } UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); - device.takeScreenshot(new File(path, testName + ".png")); + device.takeScreenshot(new File(path, testName + "." + Build.VERSION.SDK_INT + ".png")); } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2dccb5f50c..54c92bdeeb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,13 +7,13 @@ - + - - - - - - + + + + + + @@ -113,10 +114,25 @@ android:name=".service.WalletConnectService" android:enabled="true" /> + + + + + + + + + + + + + + + + + + + + NewStringUTF(env, key); -#endif -} - -JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_repository_EthereumNetworkBase_getInfuraKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getInfuraKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) return getDecryptedKey(env, infuraKey); @@ -58,65 +47,56 @@ Java_com_alphawallet_app_repository_EthereumNetworkBase_getInfuraKey( JNIEnv* en } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TickerService_getAmberDataKey( JNIEnv* env, jobject thiz ) -{ -#if (HAS_KEYS == 1) - return getDecryptedKey(env, amberdataKey); -#else - const jstring key = "obtain-api-key-from-amberdata-io"; - return (*env)->NewStringUTF(env, key); -#endif -} - -JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TickerService_getCMCKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getAnalyticsKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getDecryptedKey(env, cmcKey); + return getDecryptedKey(env, mixpanelKey); #else - const jstring key = "ea2d0a6b-7e77-4015-bccf-4877e5c5b882"; + const jstring key = "d4c1140e21f6204184bb1ea02eb84412"; return (*env)->NewStringUTF(env, key); #endif } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_AnalyticsService_getAnalyticsKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getRampKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getDecryptedKey(env, mixpanelKey); + return getDecryptedKey(env, rampKey); #else - const jstring key = "d4c1140e21f6204184bb1ea02eb84412"; + const jstring key = "asfjkdhvcmbnekjfhskjdhfskjdhfskjdhfsdkjf"; // <-- replace with your Ramp key return (*env)->NewStringUTF(env, key); #endif } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_viewmodel_Erc20DetailViewModel_getRampKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getCoinbasePayAppId( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getDecryptedKey(env, rampKey); + return getDecryptedKey(env, coinbasePayAppId); #else - const jstring key = "asfjkdhvcmbnekjfhskjdhfskjdhfskjdhfsdkjf"; // <-- replace with your Ramp key + const jstring key = ""; // <-- replace with your Coinbase Pay app id return (*env)->NewStringUTF(env, key); #endif } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_repository_OnRampRepository_getRampKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getSecondaryInfuraKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getDecryptedKey(env, rampKey); + return getDecryptedKey(env, secondaryInfuraKey); +#elif (HAS_INFURA == 1) + return (*env)->NewStringUTF(env, IFKEY); #else - const jstring key = "asfjkdhvcmbnekjfhskjdhfskjdhfskjdhfsdkjf"; // <-- replace with your Ramp key + const jstring key = "da3717f25f824cc1baa32d812386d93f"; return (*env)->NewStringUTF(env, key); #endif } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_repository_EthereumNetworkBase_getSecondaryInfuraKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getTertiaryInfuraKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getDecryptedKey(env, secondaryInfuraKey); + return getDecryptedKey(env, tertiaryInfuraKey); #elif (HAS_INFURA == 1) return (*env)->NewStringUTF(env, IFKEY); #else @@ -126,28 +106,27 @@ Java_com_alphawallet_app_repository_EthereumNetworkBase_getSecondaryInfuraKey( J } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TransactionsNetworkClient_getBSCExplorerKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getKlaytnKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getBSCExplorerKey(env); + return getDecryptedKey(env, klaytnKey); #else return (*env)->NewStringUTF(env, ""); #endif } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TransactionsNetworkClient_getEtherscanKey( JNIEnv* env, jclass thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getBSCExplorerKey( JNIEnv* env, jobject thiz ) { #if (HAS_KEYS == 1) - return getDecryptedKey(env, etherscanKey); + return getBSCExplorerKey(env); #else - const jstring key = "6U31FTHW3YYHKW6CYHKKGDPHI9HEJ9PU5F"; - return (*env)->NewStringUTF(env, key); + return (*env)->NewStringUTF(env, ""); #endif } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_GasService_getEtherscanKey( JNIEnv* env, jobject thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getEtherscanKey( JNIEnv* env, jclass thiz ) { #if (HAS_KEYS == 1) return getDecryptedKey(env, etherscanKey); @@ -158,7 +137,8 @@ Java_com_alphawallet_app_service_GasService_getEtherscanKey( JNIEnv* env, jobjec } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_widget_EmailPromptView_getMailchimpKey(JNIEnv *env, jclass clazz) { +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getMailchimpKey(JNIEnv *env, jclass clazz) +{ #if (HAS_KEYS == 1) return getDecryptedKey(env, mailchimpKey); #else @@ -168,7 +148,7 @@ Java_com_alphawallet_app_widget_EmailPromptView_getMailchimpKey(JNIEnv *env, jcl } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_GasService_getPolygonScanKey(JNIEnv *env, jobject thiz) { +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getPolygonScanKey(JNIEnv *env, jobject thiz) { #if (HAS_KEYS == 1) return getDecryptedKey(env, polygonScanKey); #elif (HAS_PS == 1) @@ -180,13 +160,7 @@ Java_com_alphawallet_app_service_GasService_getPolygonScanKey(JNIEnv *env, jobje } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TransactionsNetworkClient_getPolygonScanKey( JNIEnv* env, jclass thiz ) -{ - return Java_com_alphawallet_app_service_GasService_getPolygonScanKey(env, thiz); -} - -JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TransactionsNetworkClient_getCovalentKey( JNIEnv* env, jclass thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getCovalentKey( JNIEnv* env, jclass thiz ) { #if (HAS_KEYS == 1) return getDecryptedCKey(env, 4, '_', covalentKey); @@ -197,7 +171,7 @@ Java_com_alphawallet_app_service_TransactionsNetworkClient_getCovalentKey( JNIEn } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_TransactionsNetworkClient_getAuroraScanKey( JNIEnv* env, jclass thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getAuroraScanKey( JNIEnv* env, jclass thiz ) { #if (HAS_KEYS == 1) return getDecryptedKey(env, auroraKey); @@ -210,7 +184,7 @@ Java_com_alphawallet_app_service_TransactionsNetworkClient_getAuroraScanKey( JNI } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_service_OpenSeaService_getOpenSeaKey( JNIEnv* env, jclass thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getOpenSeaKey( JNIEnv* env, jclass thiz ) { #if (HAS_KEYS == 1) return getDecryptedKey(env, openSeaKey); @@ -223,7 +197,32 @@ Java_com_alphawallet_app_service_OpenSeaService_getOpenSeaKey( JNIEnv* env, jcla } JNIEXPORT jstring JNICALL -Java_com_alphawallet_app_util_AWEnsResolver_getOpenSeaKey( JNIEnv* env, jclass thiz ) +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getWalletConnectProjectId( JNIEnv* env, jclass thiz ) { - return Java_com_alphawallet_app_service_OpenSeaService_getOpenSeaKey(env, thiz); -} \ No newline at end of file +#if (HAS_KEYS == 1) + return getDecryptedKey(env, walletConnectProjectId); +#else + return (*env)->NewStringUTF(env, WALLETCONNECT_PROJECT_ID); +#endif +} + +JNIEXPORT jstring JNICALL +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getInfuraSecret(JNIEnv *env, jobject thiz) { +#if (HAS_KEYS == 1) + return getDecryptedKey(env, infuraSecret); +#else + const jstring key = ""; + return (*env)->NewStringUTF(env, key); +#endif +} + +JNIEXPORT jstring JNICALL +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getUnstoppableDomainsKey( JNIEnv* env, jclass thiz ) +{ +#if (HAS_KEYS == 1) + return getDecryptedKey(env, unstoppableDomainsKey); +#else + const jstring key = ""; + return (*env)->NewStringUTF(env, key); +#endif +} diff --git a/app/src/main/java/com/alphawallet/app/App.java b/app/src/main/java/com/alphawallet/app/App.java index 04713efcc1..dbbbe33fc1 100644 --- a/app/src/main/java/com/alphawallet/app/App.java +++ b/app/src/main/java/com/alphawallet/app/App.java @@ -3,17 +3,23 @@ import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; +import android.app.Activity; import android.app.Application; import android.app.UiModeManager; import android.content.Context; +import android.os.Bundle; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.PreferenceManager; import com.alphawallet.app.util.ReleaseTree; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; + +import java.util.Stack; + +import javax.inject.Inject; import dagger.hilt.android.HiltAndroidApp; -import io.reactivex.functions.Consumer; import io.reactivex.plugins.RxJavaPlugins; import io.realm.Realm; import timber.log.Timber; @@ -21,11 +27,27 @@ @HiltAndroidApp public class App extends Application { + @Inject + AWWalletConnectClient awWalletConnectClient; + + private static App mInstance; + private final Stack activityStack = new Stack<>(); + + public static App getInstance() + { + return mInstance; + } + + public Activity getTopActivity() + { + return activityStack.peek(); + } @Override public void onCreate() { super.onCreate(); + mInstance = this; Realm.init(this); if (BuildConfig.DEBUG) @@ -37,7 +59,8 @@ public void onCreate() Timber.plant(new ReleaseTree()); } - int defaultTheme = PreferenceManager.getDefaultSharedPreferences(this).getInt("theme", C.THEME_AUTO); + int defaultTheme = PreferenceManager.getDefaultSharedPreferences(this) + .getInt("theme", C.THEME_AUTO); if (defaultTheme == C.THEME_LIGHT) { @@ -63,9 +86,79 @@ else if (mode == UiModeManager.MODE_NIGHT_NO) RxJavaPlugins.setErrorHandler(Timber::e); - // enable pin code for the application -// LockManager lockManager = LockManager.getInstance(); -// lockManager.enableAppLock(this, CustomPinActivity.class); -// lockManager.getAppLock().setShouldShowForgot(false); + try + { + awWalletConnectClient.init(this); + } + catch (Exception e) + { + Timber.tag("WalletConnect").e(e); + } + + registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() + { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) + { + } + + @Override + public void onActivityDestroyed(Activity activity) + { + } + + @Override + public void onActivityStarted(Activity activity) + { + } + + @Override + public void onActivityResumed(Activity activity) + { + activityStack.push(activity); + } + + @Override + public void onActivityPaused(Activity activity) + { + pop(); + } + + @Override + public void onActivityStopped(Activity activity) + { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) + { + } + }); + } + + @Override + public void onTrimMemory(int level) + { + super.onTrimMemory(level); + if (awWalletConnectClient != null) + { + awWalletConnectClient.shutdown(); + } + } + + @Override + public void onTerminate() + { + super.onTerminate(); + activityStack.clear(); + if (awWalletConnectClient != null) + { + awWalletConnectClient.shutdown(); + } + } + + private void pop() + { + activityStack.pop(); } } diff --git a/app/src/main/java/com/alphawallet/app/C.java b/app/src/main/java/com/alphawallet/app/C.java index 13ce6f931a..092ddbc5c4 100644 --- a/app/src/main/java/com/alphawallet/app/C.java +++ b/app/src/main/java/com/alphawallet/app/C.java @@ -24,10 +24,6 @@ public abstract class C { public static final String CLASSIC_NETWORK_NAME = "Ethereum Classic"; public static final String POA_NETWORK_NAME = "POA"; public static final String XDAI_NETWORK_NAME = "Gnosis"; - public static final String KOVAN_NETWORK_NAME = "Kovan (Test)"; - public static final String ROPSTEN_NETWORK_NAME = "Ropsten (Test)"; - public static final String SOKOL_NETWORK_NAME = "Sokol (Test)"; - public static final String RINKEBY_NETWORK_NAME = "Rinkeby (Test)"; public static final String GOERLI_NETWORK_NAME = "Görli (Test)"; public static final String ARTIS_SIGMA1_NETWORK = "ARTIS sigma1"; public static final String ARTIS_TAU1_NETWORK = "ARTIS tau1 (Test)"; @@ -39,13 +35,11 @@ public abstract class C { public static final String FANTOM_TEST_NETWORK = "Fantom (Test)"; public static final String AVALANCHE_NETWORK = "Avalanche"; public static final String FUJI_TEST_NETWORK = "Avalanche FUJI (Test)"; - public static final String MATIC_NETWORK = "Polygon"; - public static final String MATIC_TEST_NETWORK = "Mumbai (Test)"; + public static final String POLYGON_NETWORK = "Polygon"; + public static final String POLYGON_TEST_NETWORK = "Mumbai (Test)"; public static final String OPTIMISTIC_NETWORK = "Optimistic"; - public static final String OPTIMISTIC_TEST_NETWORK = "Optimistic (Test)"; public static final String CRONOS_MAIN_NETWORK = "Cronos"; public static final String CRONOS_TEST_NETWORK = "Cronos (Test)"; - public static final String ARBITRUM_TEST_NETWORK = "Arbitrum (Test)"; public static final String ARBITRUM_ONE_NETWORK = "Arbitrum One"; public static final String PALM_NAME = "PALM"; public static final String PALM_TEST_NAME = "PALM (Test)"; @@ -57,7 +51,9 @@ public abstract class C { public static final String AURORA_TESTNET_NAME = "Aurora (Test)"; public static final String MILKOMEDA_NAME = "Milkomeda Cardano"; public static final String MILKOMEDA_TESTNET_NAME = "Milkomeda Cardano (Test)"; - public static final String PHI_NETWORK_NAME = "PHI"; + public static final String SEPOLIA_TESTNET_NAME = "Sepolia (Test)"; + public static final String OPTIMISM_GOERLI_TESTNET_NAME = "Optimism Goerli (Test)"; + public static final String ARBITRUM_GOERLI_TESTNET_NAME = "Arbitrum Goerli (Test)"; public static final String ETHEREUM_TICKER_NAME = "ethereum"; public static final String CLASSIC_TICKER_NAME = "ethereum-classic"; @@ -80,17 +76,18 @@ public abstract class C { public static final String HECO_SYMBOL = "HT"; public static final String FANTOM_SYMBOL = "FTM"; public static final String AVALANCHE_SYMBOL = "AVAX"; - public static final String MATIC_SYMBOL = "MATIC"; + public static final String POLYGON_SYMBOL = "MATIC"; public static final String CRONOS_SYMBOL = "CRO"; public static final String CRONOS_TEST_SYMBOL = "tCRO"; public static final String ARBITRUM_SYMBOL = "AETH"; - public static final String ARBITRUM_TEST_SYMBOL = "ARETH"; public static final String PALM_SYMBOL = "PALM"; public static final String KLAYTN_SYMBOL = "KLAY"; public static final String IOTEX_SYMBOL = "IOTX"; public static final String MILKOMEDA_SYMBOL = "milkADA"; public static final String MILKOMEDA_TEST_SYMBOL = "milktADA"; - public static final String PHI_NETWORK_SYMBOL = "\u03d5"; + public static final String SEPOLIA_SYMBOL = "ETH"; + public static final String OPTIMISM_GOERLI_TEST_SYMBOL = "ETH"; + public static final String ARBITRUM_GOERLI_TEST_SYMBOL = "AGOR"; public static final String BURN_ADDRESS = "0x0000000000000000000000000000000000000000"; @@ -105,6 +102,8 @@ public abstract class C { public static final String QUICKSWAP_EXCHANGE_DAPP = "https://quickswap.exchange/#/swap"; public static final String ONEINCH_EXCHANGE_DAPP = "https://app.1inch.io/#/[CHAIN]/swap/[TOKEN1]/[TOKEN2]"; + public static final String GLIDE_URL_INVALID = "com.bumptech.glide.load.HttpException"; + public static final String GWEI_UNIT = "Gwei"; public static final String MARKET_SALE = "market"; @@ -173,8 +172,6 @@ public abstract class C { "com.stormbird.wallet.ADDED"; public static final String CHANGED_LOCALE = "com.stormbird.wallet.CHANGED_LOCALE"; - public static final String DOWNLOAD_READY = - "com.stormbird.wallet.DOWNLOAD_READY"; public static final String PAGE_LOADED = "com.stormbird.wallet.PAGE_LOADED"; public static final String RESET_TOOLBAR = @@ -210,6 +207,8 @@ public abstract class C { public static final String SETTINGS_INSTANTIATED = "com.stormbird.wallet.SETTINGS_INSTANTIATED"; public static final String APP_FOREGROUND_STATE = "com.alphawallet.APP_FOREGROUND_STATE"; public static final String EXTRA_APP_FOREGROUND = "com.alphawallet.IS_FOREGORUND"; + public static final String SIGNAL_NFT_SYNC = "com.alphawallet.SYNC_NFT"; + public static final String SYNC_STATUS = "com.alphawallet.SYNC_STATUS"; public static final String DEFAULT_GAS_PRICE = "10000000000"; public static final String DEFAULT_XDAI_GAS_PRICE = "1000000000"; @@ -245,7 +244,10 @@ public interface ErrorCode { // Swap Error Codes int INSUFFICIENT_BALANCE = 5; - int SWAP_API_ERROR = 6; + int SWAP_CHAIN_ERROR = 6; + int SWAP_CONNECTIONS_ERROR = 7; + int SWAP_QUOTE_ERROR = 8; + int SWAP_TIMEOUT_ERROR = 9; } public interface Key { @@ -290,17 +292,12 @@ public enum TokenStatus { //Analytics public static final String PREF_UNIQUE_ID = "unique_id"; - public static final String AN_IMPORT_WALLET = "Wallet Imported"; - public static final String AN_WALLET_TYPE = "Wallet Type"; - public static final String AN_SEED_PHRASE = "Seed Phrase"; - public static final String AN_KEYSTORE = "Keystore"; - public static final String AN_PRIVATE_KEY = "Private Key"; - public static final String AN_USE_GAS = "Gas Settings"; - public static final String AN_CALL_ACTIONSHEET = "Use ActionSheet"; - public static final String AN_USE_ONRAMP = "Use OnRamp"; public static final String APP_NAME = "PACKAGE_NAME"; - public static final String ALPHAWALLET_LOGO_URI = "https://alphawallet.com/wp-content/themes/alphawallet/img/alphawallet-logo.svg"; + public static final String ALPHAWALLET_LOGO_URI = "https://alphawallet.com/wp-content/themes/alphawallet/img/logo-horizontal-new.svg"; + public static final String ALPHAWALLET_WEBSITE = "https://alphawallet.com"; + public static final String WALLET_CONNECT_REACT_APP_RELAY_URL = "wss://relay.walletconnect.com"; + public static final String ALPHA_WALLET_LOGO_URL = "https://user-images.githubusercontent.com/51817359/158344418-c0f2bd19-38bb-4e64-a1d5-25ceb099688a.png"; // Theme/Dark Mode public static final int THEME_LIGHT = 0; @@ -309,19 +306,23 @@ public enum TokenStatus { // OpenSea APIs public static final String OPENSEA_COLLECTION_API_MAINNET = "https://api.opensea.io/collection/"; + public static final String OPENSEA_ASSETS_API_MAINNET = "https://api.opensea.io/api/v1/assets"; - public static final String OPENSEA_ASSETS_API_RINKEBY = "https://testnets-api.opensea.io/api/v1/assets"; + public static final String OPENSEA_ASSETS_API_TESTNET = "https://testnets-api.opensea.io/api/v1/assets"; public static final String OPENSEA_ASSETS_API_MATIC = "https://api.opensea.io/api/v2/assets/matic"; + public static final String OPENSEA_ASSETS_API_ARBITRUM = "https://api.opensea.io/api/v2/assets/arbitrum"; + public static final String OPENSEA_ASSETS_API_AVALANCHE = "https://api.opensea.io/api/v2/assets/avalanche"; + public static final String OPENSEA_ASSETS_API_KLAYTN = "https://api.opensea.io/api/v2/assets/klaytn"; + public static final String OPENSEA_ASSETS_API_OPTIMISM = "https://api.opensea.io/api/v2/assets/optimism"; + public static final String OPENSEA_SINGLE_ASSET_API_MAINNET = "https://api.opensea.io/api/v1/asset/"; - public static final String OPENSEA_SINGLE_ASSET_API_RINKEBY = "https://testnets-api.opensea.io/api/v1/asset/"; + public static final String OPENSEA_SINGLE_ASSET_API_TESTNET = "https://testnets-api.opensea.io/api/v1/asset/"; public static final String OPENSEA_SINGLE_ASSET_API_MATIC = "https://api.opensea.io/api/v2/metadata/matic/"; + public static final String OPENSEA_SINGLE_ASSET_API_ARBITRUM = "https://api.opensea.io/api/v2/metadata/arbitrum/"; + public static final String OPENSEA_SINGLE_ASSET_API_AVALANCHE = "https://api.opensea.io/api/v2/metadata/avalanche/"; + public static final String OPENSEA_SINGLE_ASSET_API_KLAYTN = "https://api.opensea.io/api/v2/metadata/klaytn/"; + public static final String OPENSEA_SINGLE_ASSET_API_OPTIMISM = "https://api.opensea.io/api/v2/metadata/optimism/"; - // Progress Info - public interface ProgressInfo { - int FETCHING_CHAINS = 1; - int FETCHING_CONNECTIONS = 2; - int FETCHING_QUOTE = 3; - } //Timing public static long CONNECT_TIMEOUT = 10; //Seconds @@ -329,4 +330,6 @@ public interface ProgressInfo { public static long WRITE_TIMEOUT = 10; public static long PING_INTERVAL = 10; public static final long LONG_WRITE_TIMEOUT = 30; + + public static final String EXTERNAL_APP_DOWNLOAD_LINK = "https://alphawallet.com/download/AlphaWallet-release-build.apk"; } diff --git a/app/src/main/java/com/alphawallet/app/analytics/Analytics.java b/app/src/main/java/com/alphawallet/app/analytics/Analytics.java new file mode 100644 index 0000000000..cd720a05ff --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/analytics/Analytics.java @@ -0,0 +1,171 @@ +package com.alphawallet.app.analytics; + +public class Analytics +{ + // Properties + public static final String PROPS_IMPORT_WALLET_TYPE = "import_wallet_type"; + public static final String PROPS_WALLET_TYPE = "wallet_type"; + public static final String PROPS_GAS_SPEED = "gas_speed"; + public static final String PROPS_URL = "url"; + public static final String PROPS_QR_SCAN_SOURCE = "qr_scan_source"; + public static final String PROPS_ACTION_SHEET_SOURCE = "action_sheet_source"; + public static final String PROPS_ACTION_SHEET_MODE = "action_sheet_mode"; + public static final String PROPS_SWAP_FROM_TOKEN = "from_token"; + public static final String PROPS_SWAP_TO_TOKEN = "to_token"; + public static final String PROPS_ERROR_MESSAGE = "error_message"; + + public static final String PROPS_CUSTOM_NETWORK_NAME = "network_name"; + public static final String PROPS_CUSTOM_NETWORK_RPC_URL = "rpc_url"; + public static final String PROPS_CUSTOM_NETWORK_CHAIN_ID = "chain_id"; + public static final String PROPS_CUSTOM_NETWORK_SYMBOL = "symbol"; + public static final String PROPS_CUSTOM_NETWORK_IS_TESTNET = "is_testnet"; + + public enum Navigation + { + WALLET("Wallet"), + ACTIVITY("Activity"), + BROWSER("Browser"), + SETTINGS("Settings"), + MY_ADDRESS("My Wallet Address"), + MY_WALLETS("My Wallets"), + BACK_UP_WALLET("Back Up Wallet"), + SHOW_SEED_PHRASE("Show Seed Phrase"), + NAME_WALLET("Name Wallet"), + SELECT_NETWORKS("Select Networks"), + CHANGE_LANGUAGE("Change Language"), + CHANGE_CURRENCY("Change Currency"), + SETTINGS_DARK_MODE("Settings - Dark Mode"), + SETTINGS_ADVANCED("Settings - Advanced"), + SETTINGS_SUPPORT("Settings - Support"), + ADD_CUSTOM_NETWORK("Add Custom Network"), + IMPORT_WALLET("Import Wallet"), + COINBASE_PAY("Buy with Coinbase Pay"), + WALLET_CONNECT_SESSION_DETAIL("Wallet Connect Session Detail"), + WALLET_CONNECT_SESSIONS("Wallet Connect Sessions"), + ACTION_SHEET("ActionSheet"), + ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION("ActionSheet - Txn Confirmation"), + ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION_SUCCESSFUL("Txn Confirmation Successful"), + ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION_FAILED("Txn Confirmation Failed"), + TOKEN_SWAP("Token Swap"), + MY_DAPPS("My Dapps"), + ADD_DAPP("Add to My Dapps"), + EDIT_DAPP("Edit Dapp"), + BROWSER_HISTORY("Dapp Browser History"), + SCAN_QR_CODE("QR Code Scanner"); + +// SIGN_MESSAGE_REQUEST("Sign Message Request"), +// ON_RAMP("Fiat On-Ramp"), +// ON_UNISWAP("Uniswap"), +// ON_XDAI_BRIDGE("xDai Bridge"), +// ON_HONEYSWAP("Honeyswap"), +// ON_ONEINCH("Oneinch"), +// ON_CARTHAGE("Carthage"), +// ON_ARBITRUM_BRIDGE("Arbitrum Bridge"), +// ON_QUICK_SWAP("QuickSwap"), +// FALLBACK(""), +// SWITCH_SERVERS("Switch Servers"), +// TAP_BROWSER_MORE("Browser More Options"), +// EXPLORER("Explorer"), +// OPEN_SHORTCUT("Shortcut"), +// OPEN_HELP_URL("Help URL"), +// BLOCKSCAN_CHAT("Blockscan Chat"); + + private final String screenName; + + Navigation(String screenName) + { + this.screenName = screenName; + } + + public String getValue() + { + return "Screen: " + screenName; + } + } + + public enum Action + { + FIRST_WALLET_ACTION("First Wallet Action"), + IMPORT_WALLET("Import Wallet"), + USE_GAS_WIDGET("Use Gas Widget"), + LOAD_URL("Load URL"), + ACTION_SHEET_COMPLETED("ActionSheet Completed"), + ACTION_SHEET_CANCELLED("ActionSheet Cancelled"), + SCAN_QR_CODE_SUCCESS("Scan QR Code Completed"), + SCAN_QR_CODE_CANCELLED("Scan QR Code Cancelled"), + SCAN_QR_CODE_ERROR("Scan QR Code Error"), + ADD_CUSTOM_CHAIN("Add Custom Chain"), + EDIT_CUSTOM_CHAIN("Edit Custom Chain"), + WALLET_CONNECT_SESSION_REQUEST("WalletConnect - Session Request"), + WALLET_CONNECT_SESSION_APPROVED("WalletConnect - Session Approved"), + WALLET_CONNECT_SESSION_REJECTED("WalletConnect - Session Rejected"), + WALLET_CONNECT_SESSION_ENDED("WalletConnect - Session Ended"), + WALLET_CONNECT_SIGN_MESSAGE_REQUEST("WalletConnect - Sign Message Request"), + WALLET_CONNECT_SIGN_TRANSACTION_REQUEST("WalletConnect - Sign Transaction Request"), + WALLET_CONNECT_SEND_TRANSACTION_REQUEST("WalletConnect - Send Transaction Request"), + WALLET_CONNECT_SWITCH_NETWORK_REQUEST("WalletConnect - Switch Network Request"), + WALLET_CONNECT_TRANSACTION_SUCCESS("WalletConnect - Transaction Success"), + WALLET_CONNECT_TRANSACTION_FAILED("WalletConnect - Transaction Failed"), + WALLET_CONNECT_TRANSACTION_CANCELLED("WalletConnect - Transaction Cancelled"), + WALLET_CONNECT_CONNECTION_TIMEOUT("WalletConnect - Connection Timeout"), + BUY_WITH_RAMP("Buy with Ramp Clicked"), + CLEAR_BROWSER_CACHE("Clear Browser Cache"), + SHARE_URL("Share URL"), + DAPP_ADDED("Dapp Added"), + DAPP_EDITED("Dapp Edited"), + RELOAD_BROWSER("Reload Browser"), + SUPPORT_TELEGRAM("Clicked Telegram Customer Support Link"), + SUPPORT_DISCORD("Clicked Discord Link"), + SUPPORT_EMAIL("Clicked Email Link"), + SUPPORT_TWITTER("Clicked Twitter Link"), + SUPPORT_GITHUB("Clicked Github Link"), + SUPPORT_FAQ("Clicked FAQ Link"), + DEEP_LINK("Deep Link Opened"), + DEEP_LINK_API_V1("Deep Link (API V1) Opened"); + +// WALLET_CONNECT_CANCEL("WalletConnect Cancel"), +// WALLET_CONNECT_CONNECTION_FAILED("WalletConnect Connection Failed"), +// SIGN_MESSAGE_REQUEST("Sign Message Request"), +// CANCEL_SIGN_MESSAGE_REQUEST("Cancel Sign Message Request"), +// SWITCHED_SERVER("Switch Server Completed"), +// CANCELS_SWITCH_SERVER("Switch Server Cancelled"), +// RECTIFY_SEND_TRANSACTION_ERROR_IN_ACTION_SHEET("Rectify Send Txn Error"), +// ENTER_URL("Enter URL"), +// PING_INFURA("Ping Infura"), +// SUBSCRIBE_TO_EMAIL_NEWSLETTER("Subscribe Email Newsletter"), +// TAP_SAFARI_EXTENSION_REWRITTEN_URL("Tap Safari Extension Rewritten URL"); +// SUPPORT_FACEBOOK("Clicked Facebook Link"), +// SUPPORT_REDDIT("Clicked Reddit Link"), + + private final String actionName; + + Action(String actionName) + { + this.actionName = actionName; + } + + public String getValue() + { + return actionName; + } + } + + public enum Error + { + TOKEN_SWAP("Token Swap"), + TOKEN_SCRIPT("TokenScript"), + WALLET_CONNECT("WalletConnect"); + + private final String source; + + Error(String source) + { + this.source = source; + } + + public String getValue() + { + return "Error: " + source; + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java b/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java index 4e56b0fdb1..56cf427dab 100644 --- a/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java +++ b/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java @@ -1,13 +1,19 @@ package com.alphawallet.app.di; +import static com.alphawallet.app.service.KeystoreAccountService.KEYSTORE_FOLDER; + import android.content.Context; +import com.alphawallet.app.repository.CoinbasePayRepository; +import com.alphawallet.app.repository.CoinbasePayRepositoryType; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; import com.alphawallet.app.repository.OnRampRepository; import com.alphawallet.app.repository.OnRampRepositoryType; import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.repository.SharedPreferenceRepository; +import com.alphawallet.app.repository.SwapRepository; +import com.alphawallet.app.repository.SwapRepositoryType; import com.alphawallet.app.repository.TokenLocalSource; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.repository.TokenRepositoryType; @@ -25,6 +31,8 @@ import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.GasService; +import com.alphawallet.app.service.IPFSService; +import com.alphawallet.app.service.IPFSServiceType; import com.alphawallet.app.service.KeyService; import com.alphawallet.app.service.KeystoreAccountService; import com.alphawallet.app.service.NotificationService; @@ -49,181 +57,222 @@ import dagger.hilt.components.SingletonComponent; import okhttp3.OkHttpClient; -import static com.alphawallet.app.service.KeystoreAccountService.KEYSTORE_FOLDER; - @Module @InstallIn(SingletonComponent.class) -public class RepositoriesModule { - @Singleton - @Provides - PreferenceRepositoryType providePreferenceRepository(@ApplicationContext Context context) { - return new SharedPreferenceRepository(context); - } - - @Singleton - @Provides - AccountKeystoreService provideAccountKeyStoreService(@ApplicationContext Context context, KeyService keyService) { +public class RepositoriesModule +{ + @Singleton + @Provides + PreferenceRepositoryType providePreferenceRepository(@ApplicationContext Context context) + { + return new SharedPreferenceRepository(context); + } + + @Singleton + @Provides + AccountKeystoreService provideAccountKeyStoreService(@ApplicationContext Context context, KeyService keyService) + { File file = new File(context.getFilesDir(), KEYSTORE_FOLDER); - return new KeystoreAccountService(file, context.getFilesDir(), keyService); - } + return new KeystoreAccountService(file, context.getFilesDir(), keyService); + } - @Singleton + @Singleton @Provides - TickerService provideTickerService(OkHttpClient httpClient, PreferenceRepositoryType sharedPrefs, TokenLocalSource localSource) { - return new TickerService(httpClient, sharedPrefs, localSource); + TickerService provideTickerService(OkHttpClient httpClient, PreferenceRepositoryType sharedPrefs, TokenLocalSource localSource) + { + return new TickerService(httpClient, sharedPrefs, localSource); } - @Singleton - @Provides - EthereumNetworkRepositoryType provideEthereumNetworkRepository( + @Singleton + @Provides + EthereumNetworkRepositoryType provideEthereumNetworkRepository( PreferenceRepositoryType preferenceRepository, - @ApplicationContext Context context) { - return new EthereumNetworkRepository(preferenceRepository, context); - } + @ApplicationContext Context context + ) + { + return new EthereumNetworkRepository(preferenceRepository, context); + } - @Singleton - @Provides + @Singleton + @Provides WalletRepositoryType provideWalletRepository( - PreferenceRepositoryType preferenceRepositoryType, - AccountKeystoreService accountKeystoreService, - EthereumNetworkRepositoryType networkRepository, - WalletDataRealmSource walletDataRealmSource, - KeyService keyService) { - return new WalletRepository( - preferenceRepositoryType, accountKeystoreService, networkRepository, walletDataRealmSource, keyService); - } - - @Singleton - @Provides - TransactionRepositoryType provideTransactionRepository( - EthereumNetworkRepositoryType networkRepository, - AccountKeystoreService accountKeystoreService, + PreferenceRepositoryType preferenceRepositoryType, + AccountKeystoreService accountKeystoreService, + EthereumNetworkRepositoryType networkRepository, + WalletDataRealmSource walletDataRealmSource, + KeyService keyService) + { + return new WalletRepository( + preferenceRepositoryType, accountKeystoreService, networkRepository, walletDataRealmSource, keyService); + } + + @Singleton + @Provides + TransactionRepositoryType provideTransactionRepository( + EthereumNetworkRepositoryType networkRepository, + AccountKeystoreService accountKeystoreService, TransactionLocalSource inDiskCache, - TransactionsService transactionsService) { - return new TransactionRepository( - networkRepository, - accountKeystoreService, - inDiskCache, - transactionsService); - } - - @Singleton - @Provides - OnRampRepositoryType provideOnRampRepository(@ApplicationContext Context context, AnalyticsServiceType analyticsServiceType) { - return new OnRampRepository(context, analyticsServiceType); - } - - @Singleton - @Provides - TransactionLocalSource provideTransactionInDiskCache(RealmManager realmManager) { + TransactionsService transactionsService) + { + return new TransactionRepository( + networkRepository, + accountKeystoreService, + inDiskCache, + transactionsService); + } + + @Singleton + @Provides + OnRampRepositoryType provideOnRampRepository(@ApplicationContext Context context) + { + return new OnRampRepository(context); + } + + @Singleton + @Provides + SwapRepositoryType provideSwapRepository(@ApplicationContext Context context) + { + return new SwapRepository(context); + } + + @Singleton + @Provides + CoinbasePayRepositoryType provideCoinbasePayRepository() + { + return new CoinbasePayRepository(); + } + + @Singleton + @Provides + TransactionLocalSource provideTransactionInDiskCache(RealmManager realmManager) + { return new TransactionsRealmCache(realmManager); } - @Singleton - @Provides + @Singleton + @Provides TransactionsNetworkClientType provideBlockExplorerClient( - OkHttpClient httpClient, - Gson gson, - RealmManager realmManager) { - return new TransactionsNetworkClient(httpClient, gson, realmManager); - } + OkHttpClient httpClient, + Gson gson, + RealmManager realmManager) + { + return new TransactionsNetworkClient(httpClient, gson, realmManager); + } - @Singleton + @Singleton @Provides TokenRepositoryType provideTokenRepository( EthereumNetworkRepositoryType ethereumNetworkRepository, TokenLocalSource tokenLocalSource, - OkHttpClient httpClient, - @ApplicationContext Context context, - TickerService tickerService) { - return new TokenRepository( - ethereumNetworkRepository, - tokenLocalSource, - httpClient, - context, - tickerService); - } - - @Singleton - @Provides - TokenLocalSource provideRealmTokenSource(RealmManager realmManager, EthereumNetworkRepositoryType ethereumNetworkRepository) { - return new TokensRealmSource(realmManager, ethereumNetworkRepository); - } - - @Singleton - @Provides - WalletDataRealmSource provideRealmWalletDataSource(RealmManager realmManager) { - return new WalletDataRealmSource(realmManager); - } - - @Singleton - @Provides - TokensService provideTokensService(EthereumNetworkRepositoryType ethereumNetworkRepository, - TokenRepositoryType tokenRepository, - TickerService tickerService, - OpenSeaService openseaService, - AnalyticsServiceType analyticsService) { - return new TokensService(ethereumNetworkRepository, tokenRepository, tickerService, openseaService, analyticsService); - } - - @Singleton - @Provides - TransactionsService provideTransactionsService(TokensService tokensService, - EthereumNetworkRepositoryType ethereumNetworkRepositoryType, - TransactionsNetworkClientType transactionsNetworkClientType, - TransactionLocalSource transactionLocalSource) { - return new TransactionsService(tokensService, ethereumNetworkRepositoryType, transactionsNetworkClientType, transactionLocalSource); - } - - @Singleton - @Provides - GasService provideGasService(EthereumNetworkRepositoryType ethereumNetworkRepository, OkHttpClient client, RealmManager realmManager) { - return new GasService(ethereumNetworkRepository, client, realmManager); - } - - @Singleton - @Provides - OpenSeaService provideOpenseaService() { - return new OpenSeaService(); - } - - @Singleton - @Provides - SwapService provideSwapService() { - return new SwapService(); - } - - @Singleton - @Provides - AlphaWalletService provideFeemasterService(OkHttpClient okHttpClient, - TransactionRepositoryType transactionRepository, - Gson gson) { - return new AlphaWalletService(okHttpClient, transactionRepository, gson); - } - - @Singleton - @Provides - NotificationService provideNotificationService(@ApplicationContext Context ctx) { - return new NotificationService(ctx); - } - - @Singleton - @Provides - AssetDefinitionService provideAssetDefinitionService(OkHttpClient okHttpClient, @ApplicationContext Context ctx, NotificationService notificationService, RealmManager realmManager, - TokensService tokensService, TokenLocalSource tls, TransactionRepositoryType trt, - AlphaWalletService alphaService) { - return new AssetDefinitionService(okHttpClient, ctx, notificationService, realmManager, tokensService, tls, trt, alphaService); - } - - @Singleton - @Provides - KeyService provideKeyService(@ApplicationContext Context ctx, AnalyticsServiceType analyticsService) { - return new KeyService(ctx, analyticsService); - } - - @Singleton - @Provides - AnalyticsServiceType provideAnalyticsService(@ApplicationContext Context ctx) { - return new AnalyticsService(ctx); - } + OkHttpClient httpClient, + @ApplicationContext Context context, + TickerService tickerService) + { + return new TokenRepository( + ethereumNetworkRepository, + tokenLocalSource, + httpClient, + context, + tickerService); + } + + @Singleton + @Provides + TokenLocalSource provideRealmTokenSource(RealmManager realmManager, EthereumNetworkRepositoryType ethereumNetworkRepository) + { + return new TokensRealmSource(realmManager, ethereumNetworkRepository); + } + + @Singleton + @Provides + WalletDataRealmSource provideRealmWalletDataSource(RealmManager realmManager) + { + return new WalletDataRealmSource(realmManager); + } + + @Singleton + @Provides + TokensService provideTokensServices(EthereumNetworkRepositoryType ethereumNetworkRepository, + TokenRepositoryType tokenRepository, + TickerService tickerService, + OpenSeaService openseaService, + AnalyticsServiceType analyticsService) + { + return new TokensService(ethereumNetworkRepository, tokenRepository, tickerService, openseaService, analyticsService); + } + + @Singleton + @Provides + IPFSServiceType provideIPFSService(OkHttpClient client) + { + return new IPFSService(client); + } + + @Singleton + @Provides + TransactionsService provideTransactionsServices(TokensService tokensService, + EthereumNetworkRepositoryType ethereumNetworkRepositoryType, + TransactionsNetworkClientType transactionsNetworkClientType, + TransactionLocalSource transactionLocalSource) + { + return new TransactionsService(tokensService, ethereumNetworkRepositoryType, transactionsNetworkClientType, transactionLocalSource); + } + + @Singleton + @Provides + GasService provideGasService(EthereumNetworkRepositoryType ethereumNetworkRepository, OkHttpClient client, RealmManager realmManager) + { + return new GasService(ethereumNetworkRepository, client, realmManager); + } + + @Singleton + @Provides + OpenSeaService provideOpenseaService() + { + return new OpenSeaService(); + } + + @Singleton + @Provides + SwapService provideSwapService() + { + return new SwapService(); + } + + @Singleton + @Provides + AlphaWalletService provideFeemasterService(OkHttpClient okHttpClient, TransactionRepositoryType transactionRepository, Gson gson) + { + return new AlphaWalletService(okHttpClient, transactionRepository, gson); + } + + @Singleton + @Provides + NotificationService provideNotificationService(@ApplicationContext Context ctx) + { + return new NotificationService(ctx); + } + + @Singleton + @Provides + AssetDefinitionService providingAssetDefinitionServices(IPFSServiceType ipfsService, @ApplicationContext Context ctx, NotificationService notificationService, RealmManager realmManager, + TokensService tokensService, TokenLocalSource tls, + AlphaWalletService alphaService) + { + return new AssetDefinitionService(ipfsService, ctx, notificationService, realmManager, tokensService, tls, alphaService); + } + + @Singleton + @Provides + KeyService provideKeyService(@ApplicationContext Context ctx, AnalyticsServiceType analyticsService) + { + return new KeyService(ctx, analyticsService); + } + + @Singleton + @Provides + AnalyticsServiceType provideAnalyticsService(@ApplicationContext Context ctx) + { + return new AnalyticsService(ctx); + } } diff --git a/app/src/main/java/com/alphawallet/app/di/ToolsModule.java b/app/src/main/java/com/alphawallet/app/di/ToolsModule.java index 63d1549f7d..c948956729 100644 --- a/app/src/main/java/com/alphawallet/app/di/ToolsModule.java +++ b/app/src/main/java/com/alphawallet/app/di/ToolsModule.java @@ -1,7 +1,11 @@ package com.alphawallet.app.di; +import android.content.Context; + import com.alphawallet.app.C; +import com.alphawallet.app.interact.WalletConnectInteract; import com.alphawallet.app.service.RealmManager; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.google.gson.Gson; import java.util.concurrent.TimeUnit; @@ -11,34 +15,46 @@ import dagger.Module; import dagger.Provides; import dagger.hilt.InstallIn; +import dagger.hilt.android.qualifiers.ApplicationContext; import dagger.hilt.components.SingletonComponent; import okhttp3.OkHttpClient; @Module @InstallIn(SingletonComponent.class) -public class ToolsModule { - - @Singleton - @Provides - Gson provideGson() { - return new Gson(); - } - - @Singleton - @Provides - OkHttpClient okHttpClient() { - return new OkHttpClient.Builder() +public class ToolsModule +{ + + @Singleton + @Provides + Gson provideGson() + { + return new Gson(); + } + + @Singleton + @Provides + OkHttpClient okHttpClient() + { + return new OkHttpClient.Builder() //.addInterceptor(new LogInterceptor()) .connectTimeout(C.CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(C.READ_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(C.WRITE_TIMEOUT, TimeUnit.SECONDS) - .retryOnConnectionFailure(false) + .retryOnConnectionFailure(false) .build(); - } + } + + @Singleton + @Provides + RealmManager provideRealmManager() + { + return new RealmManager(); + } - @Singleton + @Singleton @Provides - RealmManager provideRealmManager() { - return new RealmManager(); + AWWalletConnectClient provideAWWalletConnectClient(@ApplicationContext Context context, WalletConnectInteract walletConnectInteract) + { + return new AWWalletConnectClient(context, walletConnectInteract); } } diff --git a/app/src/main/java/com/alphawallet/app/di/ViewModelModule.java b/app/src/main/java/com/alphawallet/app/di/ViewModelModule.java index df5275c8c8..7e846157d2 100644 --- a/app/src/main/java/com/alphawallet/app/di/ViewModelModule.java +++ b/app/src/main/java/com/alphawallet/app/di/ViewModelModule.java @@ -22,6 +22,7 @@ import com.alphawallet.app.repository.TokenRepositoryType; import com.alphawallet.app.repository.TransactionRepositoryType; import com.alphawallet.app.repository.WalletRepositoryType; +import com.alphawallet.app.router.CoinbasePayRouter; import com.alphawallet.app.router.ExternalBrowserRouter; import com.alphawallet.app.router.HomeRouter; import com.alphawallet.app.router.ImportTokenRouter; @@ -98,6 +99,11 @@ MyAddressRouter provideMyAddressRouter() { return new MyAddressRouter(); } + @Provides + CoinbasePayRouter provideCoinbasePayRouter() { + return new CoinbasePayRouter(); + } + @Provides FetchTokensInteract provideFetchTokensInteract(TokenRepositoryType tokenRepository) { return new FetchTokensInteract(tokenRepository); diff --git a/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java b/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java index 6fe68d9a68..28690c775f 100644 --- a/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java +++ b/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java @@ -1,5 +1,11 @@ package com.alphawallet.app.entity; +import androidx.activity.result.ActivityResult; + +import com.alphawallet.app.web3.entity.Web3Transaction; + +import java.math.BigInteger; + /** * Created by JB on 16/01/2021. */ @@ -7,4 +13,49 @@ public interface ActionSheetInterface { void lockDragging(boolean shouldLock); void fullExpand(); + + default void success() + { + } + + default void setURL(String url) + { + } + + default void setGasEstimate(BigInteger estimate) + { + } + + default void completeSignRequest(Boolean gotAuth) + { + } + + default void setSigningWallet(String account) + { + } + + default void setIcon(String icon) + { + } + + default void transactionWritten(String hash) + { + } + + default void updateChain(long chainId) + { + } + + default Web3Transaction getTransaction() + { + throw new RuntimeException("Implement getTransaction"); + } + + default void setSignOnly() + { + } + + default void setCurrentGasIndex(ActivityResult result) + { + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/AnalyticsProperties.java b/app/src/main/java/com/alphawallet/app/entity/AnalyticsProperties.java index f1182f5993..033ba7ae14 100644 --- a/app/src/main/java/com/alphawallet/app/entity/AnalyticsProperties.java +++ b/app/src/main/java/com/alphawallet/app/entity/AnalyticsProperties.java @@ -1,64 +1,33 @@ package com.alphawallet.app.entity; -public class AnalyticsProperties { +import org.json.JSONException; +import org.json.JSONObject; - private String walletAddress; +import timber.log.Timber; - private String fromAddress; +public class AnalyticsProperties +{ + private final JSONObject props; - private String toAddress; - - private String amount; - - private String walletType; - - private String data; - - public String getWalletAddress() { - return walletAddress; - } - - public void setWalletAddress(String walletAddress) { - this.walletAddress = walletAddress; - } - - public String getFromAddress() { - return fromAddress; - } - - public void setFromAddress(String fromAddress) { - this.fromAddress = fromAddress; - } - - public String getToAddress() { - return toAddress; - } - - public void setToAddress(String toAddress) { - this.toAddress = toAddress; - } - - public String getAmount() { - return amount; - } - - public void setAmount(String amount) { - this.amount = amount; - } - - public String getWalletType() { - return walletType; - } - - public void setWalletType(String type) { - this.walletType = type; + public AnalyticsProperties() + { + props = new JSONObject(); } - public String getData() { - return data; + public void put(String key, Object value) + { + try + { + props.put(key, value); + } + catch (JSONException e) + { + Timber.e(e); + } } - public void setData(String data) { - this.data = data; + public JSONObject get() + { + return props; } } \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/BackupTokenCallback.java b/app/src/main/java/com/alphawallet/app/entity/BackupTokenCallback.java index ddca0a6d04..b317a545b6 100644 --- a/app/src/main/java/com/alphawallet/app/entity/BackupTokenCallback.java +++ b/app/src/main/java/com/alphawallet/app/entity/BackupTokenCallback.java @@ -6,6 +6,6 @@ */ public interface BackupTokenCallback { - void backUpClick(Wallet wallet); - void remindMeLater(Wallet wallet); + default void backUpClick(Wallet wallet) { } + default void remindMeLater(Wallet wallet) { }; } diff --git a/app/src/main/java/com/alphawallet/app/entity/CoinGeckoTicker.java b/app/src/main/java/com/alphawallet/app/entity/CoinGeckoTicker.java index a2087a1722..cb3087f3d2 100644 --- a/app/src/main/java/com/alphawallet/app/entity/CoinGeckoTicker.java +++ b/app/src/main/java/com/alphawallet/app/entity/CoinGeckoTicker.java @@ -45,11 +45,15 @@ public static List buildTickerList(String jsonData, String curr fiatPrice = obj.getDouble(currencyIsoSymbol.toLowerCase()); fiatChangeStr = obj.getString(currencyIsoSymbol.toLowerCase() + "_24h_change"); } - else + else if (obj.has("usd")) { fiatPrice = obj.getDouble("usd") * currentConversionRate; fiatChangeStr = obj.getString("usd_24h_change"); } + else + { + continue; //handle empty/corrupt returns + } res.add(new CoinGeckoTicker(address, fiatPrice, getFiatChange(fiatChangeStr))); } diff --git a/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java b/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java index 36579aed33..bdfaf2a92d 100644 --- a/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java +++ b/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java @@ -7,6 +7,7 @@ import com.alphawallet.app.C; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.service.IPFSService; import com.alphawallet.app.util.Utils; import org.web3j.abi.TypeReference; @@ -22,7 +23,6 @@ import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; -import okhttp3.Request; /** * Created by JB on 7/05/2022. @@ -30,7 +30,7 @@ public class ContractInteract { private final Token token; - protected static OkHttpClient client; + protected static IPFSService client; public ContractInteract(Token token) { @@ -54,21 +54,7 @@ private String loadMetaData(String tokenURI) setupClient(); - Request request = new Request.Builder() - .url(Utils.parseIPFS(tokenURI)) - .get() - .build(); - - try (okhttp3.Response response = client.newCall(request).execute()) - { - return response.body().string(); - } - catch (Exception e) - { - // - } - - return ""; + return client.getContent(tokenURI); } public NFTAsset fetchTokenMetadata(BigInteger tokenId) @@ -111,12 +97,13 @@ private static void setupClient() { if (client == null) { - client = new OkHttpClient.Builder() - .connectTimeout(C.CONNECT_TIMEOUT, TimeUnit.SECONDS) - .connectTimeout(C.READ_TIMEOUT, TimeUnit.SECONDS) - .writeTimeout(C.WRITE_TIMEOUT, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); + client = new IPFSService( + new OkHttpClient.Builder() + .connectTimeout(C.CONNECT_TIMEOUT*2, TimeUnit.SECONDS) + .readTimeout(C.READ_TIMEOUT*2, TimeUnit.SECONDS) + .writeTimeout(C.WRITE_TIMEOUT*2, TimeUnit.SECONDS) + .retryOnConnectionFailure(false) + .build()); } } } diff --git a/app/src/main/java/com/alphawallet/app/entity/ContractType.java b/app/src/main/java/com/alphawallet/app/entity/ContractType.java index 8db5c00fd0..178077e490 100644 --- a/app/src/main/java/com/alphawallet/app/entity/ContractType.java +++ b/app/src/main/java/com/alphawallet/app/entity/ContractType.java @@ -25,5 +25,6 @@ public enum ContractType ETHEREUM_INVISIBLE, MAYBE_ERC20, ERC1155, + ERC721_ENUMERABLE, CREATION //Placeholder for generic, should be at end of list } diff --git a/app/src/main/java/com/alphawallet/app/entity/CovalentTransaction.java b/app/src/main/java/com/alphawallet/app/entity/CovalentTransaction.java index 4b62d4cdd9..566f5d0d74 100644 --- a/app/src/main/java/com/alphawallet/app/entity/CovalentTransaction.java +++ b/app/src/main/java/com/alphawallet/app/entity/CovalentTransaction.java @@ -1,25 +1,17 @@ package com.alphawallet.app.entity; -import com.alphawallet.app.entity.tokenscript.EventUtils; -import com.alphawallet.app.util.Utils; -import com.alphawallet.token.tools.Numeric; +import android.text.TextUtils; -import org.web3j.protocol.Web3j; +import com.alphawallet.app.util.Utils; import java.math.BigInteger; -import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; - -import android.text.TextUtils; - /** * Created by JB on 17/05/2021. */ @@ -65,14 +57,10 @@ public Map getParams() throws Exception Param param = new Param(); param.type = lp.type; String rawValue = TextUtils.isEmpty(lp.value) || lp.value.equals("null") ? rawLogValue : lp.value; + param.value = rawValue; if (lp.type.startsWith("uint") || lp.type.startsWith("int")) { - param.valueBI = rawValue.startsWith("0x") ? Numeric.toBigInt(rawValue) : new BigInteger(rawValue); - param.value = ""; - } - else - { - param.value = rawValue; + param.valueBI = Utils.stringToBigInteger(rawValue);// rawValue.startsWith("0x") ? Numeric.toBigInt(rawValue) : new BigInteger(rawValue); } params.put(lp.name, param); @@ -153,21 +141,10 @@ private EtherscanEvent getEtherscanTransferEvent(LogEvent logEvent) throws Excep Map logParams = logEvent.getParams(); - ev.from = logParams.get("from").value; - ev.to = logParams.get("to").value; - - logParams.remove("from"); - logParams.remove("to"); - - if (logEvent.sender_contract_decimals == 0) - { - //get TokenId - ev.tokenID = logParams.values().iterator().next().valueBI.toString(); - } - else - { - ev.value = logParams.values().iterator().next().valueBI.toString(); - } + ev.from = logParams.containsKey("from") ? logParams.get("from").value : ""; + ev.to = logParams.containsKey("to") ? logParams.get("to").value : ""; + ev.tokenID = logParams.containsKey("tokenId") ? logParams.get("tokenId").valueBI.toString() : ""; + ev.value = logParams.containsKey("value") ? logParams.get("value").valueBI.toString() : ""; ev.gasUsed = gas_spent; ev.gasPrice = gas_price; diff --git a/app/src/main/java/com/alphawallet/app/entity/CustomViewSettings.java b/app/src/main/java/com/alphawallet/app/entity/CustomViewSettings.java index 119b0873ba..cf7042005f 100644 --- a/app/src/main/java/com/alphawallet/app/entity/CustomViewSettings.java +++ b/app/src/main/java/com/alphawallet/app/entity/CustomViewSettings.java @@ -43,8 +43,7 @@ public class CustomViewSettings private static final List lockedChains = Arrays.asList( //EthereumNetworkBase.MAINNET_ID //EG only show Main, xdai, classic and two testnets. Don't allow user to select any others //EthereumNetworkBase.XDAI_ID, - //EthereumNetworkBase.RINKEBY_ID, //You can mix testnets and mainnets, but probably shouldn't as it may result in people getting scammed - //EthereumNetworkBase.GOERLI_ID + //EthereumNetworkBase.GOERLI_ID //You can mix testnets and mainnets, but probably shouldn't as it may result in people getting scammed ); public static final List alwaysVisibleChains = Arrays.asList( diff --git a/app/src/main/java/com/alphawallet/app/entity/EIP1559FeeOracleResult.java b/app/src/main/java/com/alphawallet/app/entity/EIP1559FeeOracleResult.java index 4d1f894c04..29a9ccaaa6 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EIP1559FeeOracleResult.java +++ b/app/src/main/java/com/alphawallet/app/entity/EIP1559FeeOracleResult.java @@ -19,8 +19,8 @@ public class EIP1559FeeOracleResult implements Parcelable public EIP1559FeeOracleResult(BigInteger maxFee, BigInteger maxPriority, BigInteger base) { - maxFeePerGas = minOneGwei(maxFee); - maxPriorityFeePerGas = minOneGwei(maxPriority); + maxFeePerGas = fixGasPriceReturn(maxFee); // Some chains (eg Phi) have a gas price lower than 1Gwei. + maxPriorityFeePerGas = fixGasPriceReturn(maxPriority); baseFee = base; } @@ -69,4 +69,17 @@ private BigInteger minOneGwei(BigInteger input) { return input.max(BalanceUtils.gweiToWei(BigDecimal.ONE)); } + + //returns 1 gwei if null + private BigInteger fixGasPriceReturn(BigInteger input) + { + if (input == null) + { + return BalanceUtils.gweiToWei(BigDecimal.ONE); + } + else + { + return input; + } + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/EventSync.java b/app/src/main/java/com/alphawallet/app/entity/EventSync.java index 6482a02580..ad49461f10 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EventSync.java +++ b/app/src/main/java/com/alphawallet/app/entity/EventSync.java @@ -13,6 +13,7 @@ import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.BatchResponse; import org.web3j.protocol.core.DefaultBlockParameter; import org.web3j.protocol.core.Response; import org.web3j.protocol.core.methods.request.EthFilter; @@ -34,9 +35,15 @@ public class EventSync { public static final long BLOCK_SEARCH_INTERVAL = 100000L; + public static final long POLYGON_BLOCK_SEARCH_INTERVAL = 10000L; + + private static final String TAG = "EVENT_SYNC"; + private static final boolean EVENT_SYNC_DEBUGGING = false; private final Token token; + private boolean batchProcessingError = false; + public EventSync(Token token) { this.token = token; @@ -71,8 +78,17 @@ public SyncDef getSyncDef(Realm realm) case DOWNWARD_SYNC_START: //Start event sync, optimistically try the whole current event range from 1 -> LATEST eventReadStartBlock = BigInteger.ONE; eventReadEndBlock = BigInteger.valueOf(-1L); - //write the start point here - writeStartSyncBlock(realm, currentBlock.longValue()); + if (EthereumNetworkBase.isEventBlockLimitEnforced(token.tokenInfo.chainId)) + { + syncState = EventSyncState.UPWARD_SYNC; + eventReadStartBlock = currentBlock.subtract(EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId).multiply(BigInteger.valueOf(3))); + EVENT_DEBUG("Init Sync for restricted block RPC"); + } + else + { + //write the start point here + writeStartSyncBlock(realm, currentBlock.longValue()); + } break; case DOWNWARD_SYNC: //we needed to slow down the sync eventReadStartBlock = lastBlockRead.subtract(BigInteger.valueOf(readBlockSize)); @@ -85,19 +101,77 @@ public SyncDef getSyncDef(Realm realm) break; case UPWARD_SYNC_MAX: //we are syncing from the point we started the downward sync upwardSync = true; + if (EthereumNetworkBase.isEventBlockLimitEnforced(token.tokenInfo.chainId) && upwardSyncStateLost(lastBlockRead, currentBlock)) + { + syncState = EventSyncState.UPWARD_SYNC; + EVENT_DEBUG("Switch back to sync scan"); + } + eventReadStartBlock = lastBlockRead; eventReadEndBlock = BigInteger.valueOf(-1L); break; case UPWARD_SYNC: //we encountered upward sync issues upwardSync = true; eventReadStartBlock = lastBlockRead; - eventReadEndBlock = lastBlockRead.add(BigInteger.valueOf(readBlockSize)); + if (upwardSyncComplete(eventReadStartBlock, currentBlock)) //detect completion of upward sync and switch to sync_max + { + eventReadEndBlock = BigInteger.valueOf(-1L); + syncState = EventSyncState.UPWARD_SYNC_MAX; + EVENT_DEBUG("Sync complete"); + } + else + { + eventReadEndBlock = lastBlockRead.add(BigInteger.valueOf(readBlockSize)); + } break; } + // Finally adjust the event end read if required. This is placed outside the switch because it should affect + // a few different paths + eventReadEndBlock = adjustForLimitedBlockSize(eventReadStartBlock, eventReadEndBlock, currentBlock); + + // detect edge condition - it's highly unlikely but acts as a stopper in case of unexpected results + // This edge condition is where the start block read is greater than the current block. + if (eventReadStartBlock.compareTo(currentBlock) >= 0) + { + eventReadStartBlock = currentBlock.subtract(BigInteger.ONE); + eventReadEndBlock = BigInteger.valueOf(-1L); + syncState = EventSyncState.UPWARD_SYNC_MAX; + } + return new SyncDef(eventReadStartBlock, eventReadEndBlock, syncState, upwardSync); } + private boolean upwardSyncStateLost(BigInteger lastBlockRead, BigInteger currentBlock) + { + return currentBlock.subtract(lastBlockRead).compareTo(EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId)) >= 0; + } + + private boolean upwardSyncComplete(BigInteger eventReadStartBlock, BigInteger currentBlock) + { + BigInteger maxBlockRead = EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId).subtract(BigInteger.ONE); + BigInteger diff = currentBlock.subtract(eventReadStartBlock); + + return diff.compareTo(maxBlockRead) < 0; + } + + private BigInteger adjustForLimitedBlockSize(BigInteger eventReadStartBlock, BigInteger eventReadEndBlock, BigInteger currentBlock) + { + if (EthereumNetworkBase.isEventBlockLimitEnforced(token.tokenInfo.chainId)) + { + BigInteger maxBlockRead = EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId); + + long diff = currentBlock.subtract(eventReadStartBlock).longValue(); + + if (diff >= maxBlockRead.longValue()) + { + return eventReadStartBlock.add(maxBlockRead).subtract(BigInteger.ONE); + } + } + + return eventReadEndBlock; + } + public boolean handleEthLogError(Response.Error error, DefaultBlockParameter startBlock, DefaultBlockParameter endBlock, SyncDef sync, Realm realm) { if (error.getCode() == -32005) @@ -177,6 +251,11 @@ private long reduceBlockSearch(long currentBlock, BigInteger startBlock) private long getCurrentEventBlockSize(Realm instance) { + if (EthereumNetworkBase.isEventBlockLimitEnforced(token.tokenInfo.chainId)) + { + return EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId).longValue(); + } + RealmAuxData rd = instance.where(RealmAuxData.class) .equalTo("instanceKey", TokensRealmSource.databaseKey(token.tokenInfo.chainId, token.getAddress())) .findFirst(); @@ -205,8 +284,9 @@ protected EventSyncState getCurrentTokenSyncState(Realm instance) else { int state = rd.getTokenId().intValue(); - if (state >= EventSyncState.DOWNWARD_SYNC_START.ordinal() || state < EventSyncState.TOP_LIMIT.ordinal()) + if (state >= EventSyncState.DOWNWARD_SYNC_START.ordinal() && state < EventSyncState.TOP_LIMIT.ordinal()) { + EVENT_DEBUG("Read State: " + EventSyncState.values()[state]); return EventSyncState.values()[state]; } else @@ -248,6 +328,7 @@ protected long getLastEventRead(Realm instance) } else { + EVENT_DEBUG("ReadEventSync: " + rd.getResultTime()); return rd.getResultTime(); } } @@ -310,6 +391,11 @@ public void updateEventReads(Realm realm, SyncDef sync, BigInteger currentBlock, updateEventReads(realm, sync.eventReadEndBlock.longValue(), calcNewIntervalSize(sync, evReads), sync.state); } + public void resetEventReads(Realm realm) + { + updateEventReads(realm, 0, 0, EventSyncState.DOWNWARD_SYNC_START); + } + private void updateEventReads(Realm realm, long lastRead, long readInterval, EventSyncState state) { if (realm == null) return; @@ -328,6 +414,8 @@ private void updateEventReads(Realm realm, long lastRead, long readInterval, Eve rd.setResultReceivedTime(readInterval); rd.setTokenId(String.valueOf(state.ordinal())); + EVENT_DEBUG("WriteState: " + state + " " + lastRead); + r.insertOrUpdate(rd); }); } @@ -335,7 +423,7 @@ private void updateEventReads(Realm realm, long lastRead, long readInterval, Eve // If we're syncing downwards, work out what event block size we should read next private long calcNewIntervalSize(SyncDef sync, int evReads) { - if (sync.upwardSync) return BLOCK_SEARCH_INTERVAL; + if (sync.upwardSync) return EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId).longValue(); long endBlock = sync.eventReadEndBlock.longValue() == -1 ? TransactionsService.getCurrentBlock(token.tokenInfo.chainId).longValue() : sync.eventReadEndBlock.longValue(); long currentReadSize = endBlock - sync.eventReadStartBlock.longValue(); @@ -352,7 +440,7 @@ else if (evReads < 1000) } else if ((maxLogReads - evReads) > maxLogReads*0.25) { - currentReadSize += BLOCK_SEARCH_INTERVAL; + currentReadSize += EthereumNetworkBase.getMaxEventFetch(token.tokenInfo.chainId).longValue(); } return currentReadSize; @@ -360,6 +448,8 @@ else if ((maxLogReads - evReads) > maxLogReads*0.25) /*** * Event Handling + * + * TODO: batch up catch-up calls */ public Pair, HashSet>> processTransferEvents(Web3j web3j, Event transferEvent, DefaultBlockParameter startBlock, @@ -370,26 +460,18 @@ public Pair, HashSet>> processTran EthFilter receiveFilter = token.getReceiveBalanceFilter(transferEvent, startBlock, endBlock); EthFilter sendFilter = token.getSendBalanceFilter(transferEvent, startBlock, endBlock); - EthLog receiveLogs = web3j.ethGetLogs(receiveFilter).send(); - if (receiveLogs.hasError()) - { - throw new LogOverflowException(receiveLogs.getError()); - } + Pair ethLogs = getTxLogs(web3j, receiveFilter, sendFilter); + + EthLog receiveLogs = ethLogs.first; + EthLog sendLogs = ethLogs.second; int eventCount = receiveLogs.getLogs().size(); HashSet rcvTokenIds = new HashSet<>(token.processLogsAndStoreTransferEvents(receiveLogs, transferEvent, txHashes, realm)); - EthLog sentLogs = web3j.ethGetLogs(sendFilter).send(); + if (sendLogs.getLogs().size() > eventCount) eventCount = sendLogs.getLogs().size(); - if (sentLogs.hasError()) - { - throw new LogOverflowException(receiveLogs.getError()); - } - - if (sentLogs.getLogs().size() > eventCount) eventCount = sentLogs.getLogs().size(); - - HashSet sendTokenIds = token.processLogsAndStoreTransferEvents(sentLogs, transferEvent, txHashes, realm); + HashSet sendTokenIds = token.processLogsAndStoreTransferEvents(sendLogs, transferEvent, txHashes, realm); //register Transaction fetches for (String txHash : txHashes) @@ -400,6 +482,60 @@ public Pair, HashSet>> processTran return new Pair<>(eventCount, new Pair<>(rcvTokenIds, sendTokenIds)); } + private Pair getTxLogs(Web3j web3j, EthFilter receiveFilter, EthFilter sendFilter) throws LogOverflowException, IOException + { + if (EthereumNetworkBase.getBatchProcessingLimit(token.tokenInfo.chainId) > 0 && !batchProcessingError) + { + return getBatchTxLogs(web3j, receiveFilter, sendFilter); + } + else + { + EthLog receiveLogs = web3j.ethGetLogs(receiveFilter).send(); + + if (receiveLogs.hasError()) + { + throw new LogOverflowException(receiveLogs.getError()); + } + + EthLog sentLogs = web3j.ethGetLogs(sendFilter).send(); + + if (sentLogs.hasError()) + { + throw new LogOverflowException(sentLogs.getError()); + } + + return new Pair<>(receiveLogs, sentLogs); + } + } + + private Pair getBatchTxLogs(Web3j web3j, EthFilter receiveFilter, EthFilter sendFilter) throws LogOverflowException, IOException + { + BatchResponse rsp = web3j.newBatch() + .add(web3j.ethGetLogs(receiveFilter)) + .add(web3j.ethGetLogs(sendFilter)) + .send(); + + if (rsp.getResponses().size() != 2) + { + batchProcessingError = true; + return getTxLogs(web3j, receiveFilter, sendFilter); + } + + EthLog receiveLogs = (EthLog) rsp.getResponses().get(0); + EthLog sendLogs = (EthLog) rsp.getResponses().get(1); + + if (receiveLogs.hasError()) + { + throw new LogOverflowException(receiveLogs.getError()); + } + else if (sendLogs.hasError()) + { + throw new LogOverflowException(sendLogs.getError()); + } + + return new Pair<>(receiveLogs, sendLogs); + } + public String getActivityName(String toAddress) { String activityName = ""; @@ -528,4 +664,12 @@ private void storeTransferData(Realm instance, String hash, String valueList, St matchingEntry.setTransferDetail(valueList); instance.insertOrUpdate(matchingEntry); } + + private void EVENT_DEBUG(String message) + { + if (EVENT_SYNC_DEBUGGING) + { + Timber.tag(TAG).i(token.tokenInfo.chainId + " " + token.tokenInfo.address + ": " + message); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/FragmentMessenger.java b/app/src/main/java/com/alphawallet/app/entity/FragmentMessenger.java index 1565ab14e8..f736a1544d 100644 --- a/app/src/main/java/com/alphawallet/app/entity/FragmentMessenger.java +++ b/app/src/main/java/com/alphawallet/app/entity/FragmentMessenger.java @@ -7,5 +7,8 @@ public interface FragmentMessenger { void tokenScriptError(String message); - void updateReady(int versionUpdate); + + void playStoreUpdateReady(int versionUpdate); + + void externalUpdateReady(String version); } diff --git a/app/src/main/java/com/alphawallet/app/entity/FunctionData.java b/app/src/main/java/com/alphawallet/app/entity/FunctionData.java index a042435363..d82f6bed9d 100644 --- a/app/src/main/java/com/alphawallet/app/entity/FunctionData.java +++ b/app/src/main/java/com/alphawallet/app/entity/FunctionData.java @@ -1,14 +1,14 @@ package com.alphawallet.app.entity; -import com.alphawallet.app.C; -import com.alphawallet.app.widget.FunctionButtonBar; +import static com.alphawallet.app.entity.ContractType.CREATION; +import static com.alphawallet.app.entity.ContractType.ERC20; +import static com.alphawallet.app.entity.ContractType.ERC875; +import static com.alphawallet.app.entity.ContractType.ERC875_LEGACY; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static com.alphawallet.app.entity.ContractType.*; - /** * Created by James on 2/02/2018. */ diff --git a/app/src/main/java/com/alphawallet/app/entity/GenericCallback.java b/app/src/main/java/com/alphawallet/app/entity/GenericCallback.java new file mode 100644 index 0000000000..1511aacf79 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/GenericCallback.java @@ -0,0 +1,9 @@ +package com.alphawallet.app.entity; + +/** A generic functional interface to supply a result of an async operation + * @param Result required by caller + */ +public interface GenericCallback +{ + void call(T t); +} diff --git a/app/src/main/java/com/alphawallet/app/entity/HomeCommsInterface.java b/app/src/main/java/com/alphawallet/app/entity/HomeCommsInterface.java index 2de9a623d9..af9233bf60 100644 --- a/app/src/main/java/com/alphawallet/app/entity/HomeCommsInterface.java +++ b/app/src/main/java/com/alphawallet/app/entity/HomeCommsInterface.java @@ -2,7 +2,6 @@ public interface HomeCommsInterface { - void downloadReady(String ready); void requestNotificationPermission(); void backupSuccess(String keyAddress); void resetTokens(); diff --git a/app/src/main/java/com/alphawallet/app/entity/HomeReceiver.java b/app/src/main/java/com/alphawallet/app/entity/HomeReceiver.java index 2fedbaa754..fa06a12fbf 100644 --- a/app/src/main/java/com/alphawallet/app/entity/HomeReceiver.java +++ b/app/src/main/java/com/alphawallet/app/entity/HomeReceiver.java @@ -27,10 +27,6 @@ public void onReceive(Context context, Intent intent) Bundle bundle = intent.getExtras(); switch (intent.getAction()) { - case C.DOWNLOAD_READY: - String message = bundle.getString("Version"); - homeCommsInterface.downloadReady(message); - break; case C.REQUEST_NOTIFICATION_ACCESS: homeCommsInterface.requestNotificationPermission(); break; @@ -49,7 +45,6 @@ public void onReceive(Context context, Intent intent) public void register() { IntentFilter filter = new IntentFilter(); - filter.addAction(C.DOWNLOAD_READY); filter.addAction(C.REQUEST_NOTIFICATION_ACCESS); filter.addAction(C.BACKUP_WALLET_SUCCESS); filter.addAction(C.WALLET_CONNECT_REQUEST); diff --git a/app/src/main/java/com/alphawallet/app/entity/QRResult.java b/app/src/main/java/com/alphawallet/app/entity/QRResult.java index 59b8a94039..50c3f6ed38 100644 --- a/app/src/main/java/com/alphawallet/app/entity/QRResult.java +++ b/app/src/main/java/com/alphawallet/app/entity/QRResult.java @@ -2,7 +2,9 @@ import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; + +import com.alphawallet.app.util.Utils; + import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; @@ -161,6 +163,11 @@ else if (params.size() == 2) { type = EIP681Type.OTHER_PROTOCOL; } + else if (isEIP681() && Utils.isAddressValid(address)) //Metamask addresses + { + type = EIP681Type.ADDRESS; + return; + } else { type = EIP681Type.OTHER; diff --git a/app/src/main/java/com/alphawallet/app/entity/QueryResponse.java b/app/src/main/java/com/alphawallet/app/entity/QueryResponse.java new file mode 100644 index 0000000000..a232a2f2db --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/QueryResponse.java @@ -0,0 +1,23 @@ +package com.alphawallet.app.entity; + +/** + * Created by JB on 5/11/2022. + */ +public class QueryResponse +{ + public final int code; + public final String body; + + public QueryResponse(int code, String body) + { + this.code = code; + this.body = body; + } + + public boolean isSuccessful() + { + return code >= 200 && code <= 299; + } +} + + diff --git a/app/src/main/java/com/alphawallet/app/entity/SignAuthenticationCallback.java b/app/src/main/java/com/alphawallet/app/entity/SignAuthenticationCallback.java index cc8ae9e2f8..440998c77e 100644 --- a/app/src/main/java/com/alphawallet/app/entity/SignAuthenticationCallback.java +++ b/app/src/main/java/com/alphawallet/app/entity/SignAuthenticationCallback.java @@ -1,7 +1,5 @@ package com.alphawallet.app.entity; -import com.alphawallet.token.entity.Signable; - /** * Created by James on 21/07/2019. * Stormbird in Sydney @@ -9,8 +7,7 @@ public interface SignAuthenticationCallback { void gotAuthorisation(boolean gotAuth); - default void gotAuthorisationForSigning(boolean gotAuth, Signable messageToSign) { } //if you implement message signing default void createdKey(String keyAddress) { } void cancelAuthentication(); -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/entity/Transaction.java b/app/src/main/java/com/alphawallet/app/entity/Transaction.java index 2108cf9119..18037a924c 100644 --- a/app/src/main/java/com/alphawallet/app/entity/Transaction.java +++ b/app/src/main/java/com/alphawallet/app/entity/Transaction.java @@ -28,14 +28,13 @@ import java.util.Map; /** - * * This is supposed to be a generic transaction class which can * contain all of 3 stages of a transaction: - * + *

* 1. being compiled, in progress, or ready to be signed; * 2. compiled and signed, or ready to be broadcasted; - * 2. already broadcasted, obtained in its raw format from a node, - * including the signatures in it; + * 2. already broadcasted, obtained in its raw format from a node, + * including the signatures in it; * 4. already included in a blockchain. */ public class Transaction implements Parcelable @@ -61,654 +60,666 @@ public class Transaction implements Parcelable public TransactionInput transactionInput = null; public static final String CONSTRUCTOR = "Constructor"; - public static final TransactionDecoder decoder = new TransactionDecoder(); - public static ParseMagicLink parser = null; - - //placeholder for error - public Transaction() - { - //blank transaction - hash = ""; - blockNumber = ""; - timeStamp = 0; - nonce = 0; - from = ""; - to = ""; - value = ""; - gas = ""; - gasPrice = ""; - gasUsed = ""; - input = ""; - error = ""; - chainId = 0; - maxFeePerGas = ""; - maxPriorityFee = ""; - } - - public boolean isPending() - { - return TextUtils.isEmpty(blockNumber) || blockNumber.equals("0") || blockNumber.equals("-2"); - } - - public boolean hasError() - { - return !TextUtils.isEmpty(error) && error.equals("1"); - } - - public boolean hasData() - { - return !TextUtils.isEmpty(input) && input.length() > 2; - } + public static final TransactionDecoder decoder = new TransactionDecoder(); + public static ParseMagicLink parser = null; + + //placeholder for error + public Transaction() + { + //blank transaction + hash = ""; + blockNumber = ""; + timeStamp = 0; + nonce = 0; + from = ""; + to = ""; + value = ""; + gas = ""; + gasPrice = ""; + gasUsed = ""; + input = ""; + error = ""; + chainId = 0; + maxFeePerGas = ""; + maxPriorityFee = ""; + } + + public boolean isPending() + { + return TextUtils.isEmpty(blockNumber) || blockNumber.equals("0") || blockNumber.equals("-2"); + } + + public boolean hasError() + { + return !TextUtils.isEmpty(error) && error.equals("1"); + } + + public boolean hasData() + { + return !TextUtils.isEmpty(input) && input.length() > 2; + } public Transaction( String hash, String error, String blockNumber, long timeStamp, - int nonce, - String from, - String to, - String value, - String gas, - String gasPrice, - String input, - String gasUsed, + int nonce, + String from, + String to, + String value, + String gas, + String gasPrice, + String input, + String gasUsed, long chainId, - boolean isConstructor) { + boolean isConstructor) + { this.hash = hash; this.error = error; this.blockNumber = blockNumber; this.timeStamp = timeStamp; - this.nonce = nonce; - this.from = from; - this.to = to; - this.value = value; - this.gas = gas; - this.gasPrice = gasPrice; - this.input = input; - this.gasUsed = gasUsed; - this.chainId = chainId; - this.isConstructor = isConstructor; - this.maxFeePerGas = ""; - this.maxPriorityFee = ""; - } - - public Transaction(Web3Transaction tx, long chainId, String wallet) - { - this.hash = null; - this.error = null; - this.blockNumber = null; - this.timeStamp = System.currentTimeMillis()/1000; - this.nonce = -1; - this.from = wallet; - this.to = tx.recipient.toString(); - this.value = tx.value.toString(); - this.gas = tx.gasLimit.toString(); - this.gasPrice = tx.gasPrice.toString(); - this.input = tx.payload; - this.gasUsed = tx.gasLimit.toString(); - this.chainId = chainId; - this.isConstructor = tx.isConstructor(); - this.maxFeePerGas = tx.maxFeePerGas.toString(); - this.maxPriorityFee = tx.maxPriorityFeePerGas.toString(); - } - - public Transaction(CovalentTransaction cTx, long chainId, long transactionTime) - { - if (cTx.to_address == null || cTx.to_address.equals("null")) - { - isConstructor = true; - input = CONSTRUCTOR; - //determine creation address from events - to = cTx.determineContractAddress(); - } - else - { - to = cTx.to_address; - input = ""; - } - - this.hash = cTx.tx_hash; - this.blockNumber = cTx.block_height; - this.timeStamp = transactionTime; - this.error = cTx.successful ? "0" : "1"; - this.nonce = 0; //don't know this - this.from = cTx.from_address; - this.value = cTx.value; - this.gas = String.valueOf(cTx.gas_offered); - this.gasPrice = cTx.gas_price; - this.gasUsed = cTx.gas_spent; - this.chainId = chainId; - this.maxFeePerGas = ""; - this.maxPriorityFee = ""; - } - - public Transaction(org.web3j.protocol.core.methods.response.Transaction ethTx, long chainId, boolean isSuccess, long timeStamp) - { - // Get contract address if constructor - String contractAddress = ethTx.getCreates() != null ? ethTx.getCreates() : ""; - int nonce = ethTx.getNonceRaw() != null ? Numeric.toBigInt(ethTx.getNonceRaw()).intValue() : 0; - - if (!TextUtils.isEmpty(contractAddress)) //must be a constructor - { - to = contractAddress; - isConstructor = true; - input = CONSTRUCTOR; - } - else if (ethTx.getTo() == null && ethTx.getInput() != null && ethTx.getInput().startsWith("0x60")) - { - // some clients don't populate the 'creates' data for constructors. Note: Ethereum constructor always starts with a 'PUSH' 0x60 instruction - input = CONSTRUCTOR; - isConstructor = true; - to = calculateContractAddress(ethTx.getFrom(), nonce); - } - else - { - this.to = ethTx.getTo() != null ? ethTx.getTo() : ""; - this.input = ethTx.getInput(); - } - - this.hash = ethTx.getHash(); - this.blockNumber = ethTx.getBlockNumber().toString(); - this.timeStamp = timeStamp; - this.error = isSuccess ? "0" : "1"; - this.nonce = nonce; - this.from = ethTx.getFrom(); - this.value = ethTx.getValue().toString(); - this.gas = ethTx.getGas().toString(); - this.gasPrice = ethTx.getGasPrice().toString(); - this.gasUsed = ethTx.getGas().toString(); - this.chainId = chainId; - this.maxFeePerGas = ethTx.getMaxFeePerGas(); - this.maxPriorityFee = ethTx.getMaxPriorityFeePerGas(); - } - - public Transaction(String hash, String isError, String blockNumber, long timeStamp, int nonce, String from, String to, - String value, String gas, String gasPrice, String input, String gasUsed, long chainId, String contractAddress) - { - //Is it a constructor? - if (!TextUtils.isEmpty(contractAddress)) - { - String testContractDeploymentAddress = Utils.calculateContractAddress(from, nonce); - if (testContractDeploymentAddress.equalsIgnoreCase(contractAddress)) - { - to = contractAddress; - isConstructor = true; - input = CONSTRUCTOR; - } - } - - this.to = to; - this.hash = hash; - this.error = isError; - this.blockNumber = blockNumber; - this.timeStamp = timeStamp; - this.nonce = nonce; - this.from = from; - this.value = value; - this.gas = gas; - this.gasPrice = gasPrice; - this.input = input; - this.gasUsed = gasUsed; - this.chainId = chainId; - this.maxFeePerGas = ""; - this.maxPriorityFee = ""; - } - - public Transaction(String hash, String isError, String blockNumber, long timeStamp, int nonce, String from, String to, - String value, String gas, String gasPrice, String maxFeePerGas, String maxPriorityFee, String input, String gasUsed, long chainId, String contractAddress) - { - if (!TextUtils.isEmpty(contractAddress)) - { - String testContractDeploymentAddress = Utils.calculateContractAddress(from, nonce); - if (testContractDeploymentAddress.equalsIgnoreCase(contractAddress)) - { - to = contractAddress; - isConstructor = true; - input = CONSTRUCTOR; - } - } - - this.to = to; - this.hash = hash; - this.error = isError; - this.blockNumber = blockNumber; - this.timeStamp = timeStamp; - this.nonce = nonce; - this.from = from; - this.value = value; - this.gas = gas; - this.maxFeePerGas = maxFeePerGas; - this.maxPriorityFee = maxPriorityFee; - this.gasPrice = gasPrice; - this.input = input; - this.gasUsed = gasUsed; - this.chainId = chainId; - } - - protected Transaction(Parcel in) - { - hash = in.readString(); - error = in.readString(); - blockNumber = in.readString(); - timeStamp = in.readLong(); - nonce = in.readInt(); - from = in.readString(); - to = in.readString(); - value = in.readString(); - gas = in.readString(); - gasPrice = in.readString(); - input = in.readString(); - gasUsed = in.readString(); - chainId = in.readLong(); - maxFeePerGas = in.readString(); - maxPriorityFee = in.readString(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public Transaction createFromParcel(Parcel in) { - return new Transaction(in); - } - - @Override - public Transaction[] newArray(int size) { - return new Transaction[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { + this.nonce = nonce; + this.from = from; + this.to = to; + this.value = value; + this.gas = gas; + this.gasPrice = gasPrice; + this.input = input; + this.gasUsed = gasUsed; + this.chainId = chainId; + this.isConstructor = isConstructor; + this.maxFeePerGas = ""; + this.maxPriorityFee = ""; + } + + public Transaction(Web3Transaction tx, long chainId, String wallet) + { + this.hash = null; + this.error = null; + this.blockNumber = null; + this.timeStamp = System.currentTimeMillis() / 1000; + this.nonce = -1; + this.from = wallet; + this.to = tx.recipient.toString(); + this.value = tx.value.toString(); + this.gas = tx.gasLimit.toString(); + this.gasPrice = tx.gasPrice.toString(); + this.input = tx.payload; + this.gasUsed = tx.gasLimit.toString(); + this.chainId = chainId; + this.isConstructor = tx.isConstructor(); + this.maxFeePerGas = tx.maxFeePerGas.toString(); + this.maxPriorityFee = tx.maxPriorityFeePerGas.toString(); + } + + public Transaction(CovalentTransaction cTx, long chainId, long transactionTime) + { + if (cTx.to_address == null || cTx.to_address.equals("null")) + { + isConstructor = true; + input = CONSTRUCTOR; + //determine creation address from events + to = cTx.determineContractAddress(); + } + else + { + to = cTx.to_address; + input = "0x"; + } + + this.hash = cTx.tx_hash; + this.blockNumber = cTx.block_height; + this.timeStamp = transactionTime; + this.error = cTx.successful ? "0" : "1"; + this.nonce = 0; //don't know this + this.from = cTx.from_address; + this.value = cTx.value; + this.gas = String.valueOf(cTx.gas_offered); + this.gasPrice = cTx.gas_price; + this.gasUsed = cTx.gas_spent; + this.chainId = chainId; + this.maxFeePerGas = ""; + this.maxPriorityFee = ""; + } + + public Transaction(org.web3j.protocol.core.methods.response.Transaction ethTx, long chainId, boolean isSuccess, long timeStamp) + { + // Get contract address if constructor + String contractAddress = ethTx.getCreates() != null ? ethTx.getCreates() : ""; + int nonce = ethTx.getNonceRaw() != null ? Numeric.toBigInt(ethTx.getNonceRaw()).intValue() : 0; + + if (!TextUtils.isEmpty(contractAddress)) //must be a constructor + { + to = contractAddress; + isConstructor = true; + input = CONSTRUCTOR; + } + else if (ethTx.getTo() == null && ethTx.getInput() != null && ethTx.getInput().startsWith("0x60")) + { + // some clients don't populate the 'creates' data for constructors. Note: Ethereum constructor always starts with a 'PUSH' 0x60 instruction + input = CONSTRUCTOR; + isConstructor = true; + to = calculateContractAddress(ethTx.getFrom(), nonce); + } + else + { + this.to = ethTx.getTo() != null ? ethTx.getTo() : ""; + this.input = ethTx.getInput(); + } + + this.hash = ethTx.getHash(); + this.blockNumber = ethTx.getBlockNumber().toString(); + this.timeStamp = timeStamp; + this.error = isSuccess ? "0" : "1"; + this.nonce = nonce; + this.from = ethTx.getFrom(); + this.value = ethTx.getValue().toString(); + this.gas = ethTx.getGas().toString(); + this.gasPrice = ethTx.getGasPrice().toString(); + this.gasUsed = ethTx.getGas().toString(); + this.chainId = chainId; + this.maxFeePerGas = ethTx.getMaxFeePerGas(); + this.maxPriorityFee = ethTx.getMaxPriorityFeePerGas(); + } + + public Transaction(String hash, String isError, String blockNumber, long timeStamp, int nonce, String from, String to, + String value, String gas, String gasPrice, String input, String gasUsed, long chainId, String contractAddress) + { + //Is it a constructor? + if (!TextUtils.isEmpty(contractAddress)) + { + String testContractDeploymentAddress = Utils.calculateContractAddress(from, nonce); + if (testContractDeploymentAddress.equalsIgnoreCase(contractAddress)) + { + to = contractAddress; + isConstructor = true; + input = CONSTRUCTOR; + } + } + + this.to = to; + this.hash = hash; + this.error = isError; + this.blockNumber = blockNumber; + this.timeStamp = timeStamp; + this.nonce = nonce; + this.from = from; + this.value = value; + this.gas = gas; + this.gasPrice = gasPrice; + this.input = input; + this.gasUsed = gasUsed; + this.chainId = chainId; + this.maxFeePerGas = ""; + this.maxPriorityFee = ""; + } + + public Transaction(String hash, String isError, String blockNumber, long timeStamp, int nonce, String from, String to, + String value, String gas, String gasPrice, String maxFeePerGas, String maxPriorityFee, String input, String gasUsed, long chainId, String contractAddress) + { + if (!TextUtils.isEmpty(contractAddress)) + { + String testContractDeploymentAddress = Utils.calculateContractAddress(from, nonce); + if (testContractDeploymentAddress.equalsIgnoreCase(contractAddress)) + { + to = contractAddress; + isConstructor = true; + input = CONSTRUCTOR; + } + } + + this.to = to; + this.hash = hash; + this.error = isError; + this.blockNumber = blockNumber; + this.timeStamp = timeStamp; + this.nonce = nonce; + this.from = from; + this.value = value; + this.gas = gas; + this.maxFeePerGas = maxFeePerGas; + this.maxPriorityFee = maxPriorityFee; + this.gasPrice = gasPrice; + this.input = input; + this.gasUsed = gasUsed; + this.chainId = chainId; + } + + protected Transaction(Parcel in) + { + hash = in.readString(); + error = in.readString(); + blockNumber = in.readString(); + timeStamp = in.readLong(); + nonce = in.readInt(); + from = in.readString(); + to = in.readString(); + value = in.readString(); + gas = in.readString(); + gasPrice = in.readString(); + input = in.readString(); + gasUsed = in.readString(); + chainId = in.readLong(); + maxFeePerGas = in.readString(); + maxPriorityFee = in.readString(); + } + + public static final Creator CREATOR = new Creator() + { + @Override + public Transaction createFromParcel(Parcel in) + { + return new Transaction(in); + } + + @Override + public Transaction[] newArray(int size) + { + return new Transaction[size]; + } + }; + + @Override + public int describeContents() + { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) + { dest.writeString(hash); dest.writeString(error); dest.writeString(blockNumber); dest.writeLong(timeStamp); - dest.writeInt(nonce); - dest.writeString(from); - dest.writeString(to); - dest.writeString(value); - dest.writeString(gas); - dest.writeString(gasPrice); - dest.writeString(input); - dest.writeString(gasUsed); - dest.writeLong(chainId); - dest.writeString(maxFeePerGas); - dest.writeString(maxPriorityFee); - } - - public boolean isRelated(String contractAddress, String walletAddress) - { - if (contractAddress.equals("eth")) - { - return (input.equals("0x") || from.equalsIgnoreCase(walletAddress)); - } - else if (walletAddress.equalsIgnoreCase(contractAddress)) //transactions sent from or sent to the main currency account - { - return from.equalsIgnoreCase(walletAddress) || to.equalsIgnoreCase(walletAddress); - } - else if (to.equalsIgnoreCase(contractAddress)) - { - return true; - } - else - { - return getWalletInvolvedInTransaction(walletAddress); - } - } - - /** - * Fetch result of transaction operation. - * This is very much a WIP - * @param token - * @return - */ - public String getOperationResult(Token token, int precision) - { - //get amount here. will be amount + symbol if appropriate - if (hasInput()) - { - decodeTransactionInput(token.getWallet()); - String value = transactionInput.getOperationValue(token, this); - boolean isSendOrReceive = !from.equalsIgnoreCase(to) && transactionInput.isSendOrReceive(this); - String prefix = (value.length() == 0 || (value.startsWith("#") || !isSendOrReceive)) ? "" : - (token.getIsSent(this) ? "- " : "+ "); - return prefix + value; - } - else - { - return token.getTransactionValue(this, precision); - } - } - - /** - * Can the contract call be valid if the operation token is Ethereum? - * @param token - * @return - */ - public boolean shouldShowSymbol(Token token) - { - if (hasInput()) - { - decodeTransactionInput(token.getWallet()); - return transactionInput.shouldShowSymbol(token); - } - else - { - return true; - } - } - - public String getOperationTokenAddress() - { - if (hasInput()) - { - return to; - } - else - { - return ""; - } - } - - public boolean isLegacyTransaction() - { - try - { - return !TextUtils.isEmpty(gasPrice) && new BigInteger(gasPrice).compareTo(BigInteger.ZERO) > 0; - } - catch (Exception e) - { - return true; - } - } - - public String getOperationName(Context ctx, Token token, String walletAddress) - { - String txName = null; - if (isPending()) - { - txName = ctx.getString(R.string.status_pending); - } - else if (hasInput()) - { - decodeTransactionInput(walletAddress); - if (token.isEthereum() && shouldShowSymbol(token)) - { - transactionInput.type = TransactionType.CONTRACT_CALL; - } - - return transactionInput.getOperationTitle(ctx); - } - - return txName; - } - - public boolean hasInput() - { - return input != null && input.length() >= 10; - } - - public int getOperationToFrom(String walletAddress) - { - if (hasInput()) - { - decodeTransactionInput(walletAddress); - return transactionInput.getOperationToFrom(); - } - else - { - return 0; - } - } - - public StatusType getOperationImage(Token token) - { - if (hasError()) - { - return StatusType.FAILED; - } - else if (hasInput()) - { - decodeTransactionInput(token.getWallet()); - return transactionInput.getOperationImage(this, token.getWallet()); - } - else - { - return from.equalsIgnoreCase(token.getWallet()) ? StatusType.SENT : StatusType.RECEIVE; - } - } - - public TransactionType getTransactionType(String wallet) - { - if (hasError()) - { - return TransactionType.UNKNOWN; - } - else if (hasInput()) - { - decodeTransactionInput(wallet); - return transactionInput.type; - } - else - { - return TransactionType.SEND_ETH; - } - } - - /** - * Supplimental info in this case is the intrinsic root value attached to a contract call - * EG: Calling cryptokitties ERC721 'breedWithAuto' function requires you to call the function and also attach a small amount of ETH - * for the 'breeding fee'. That fee is later released to the caller of the 'birth' function. - * Supplemental info for these transaction would appear as -0.031 for the 'breedWithAuto' and +0.031 on the 'birth' call - * However it's not that simple - the 'breeding fee' will be in the value attached to the transaction, however the 'midwife' reward appears - * as an internal transaction, so won't be in the 'value' property. - * - * @return - */ - public String getSupplementalInfo(String walletAddress, String networkName) - { - if (hasInput()) - { - decodeTransactionInput(walletAddress); - return transactionInput.getSupplimentalInfo(this, walletAddress, networkName); - } - else - { - return ""; - } - } - - public String getPrefix(Token token) - { - if (hasInput()) - { - decodeTransactionInput(token.getWallet()); - if (!transactionInput.isSendOrReceive(this) || token.isEthereum()) - { - return ""; - } - else if (token.isERC721()) - { - return ""; - } - } - - boolean isSent = token.getIsSent(this); - boolean isSelf = from.equalsIgnoreCase(to); - if (isSelf) return ""; - else if (isSent) return "- "; - else return "+ "; - } + dest.writeInt(nonce); + dest.writeString(from); + dest.writeString(to); + dest.writeString(value); + dest.writeString(gas); + dest.writeString(gasPrice); + dest.writeString(input); + dest.writeString(gasUsed); + dest.writeLong(chainId); + dest.writeString(maxFeePerGas); + dest.writeString(maxPriorityFee); + } + + public boolean isRelated(String contractAddress, String walletAddress) + { + if (contractAddress.equals("eth")) + { + return (input.equals("0x") || from.equalsIgnoreCase(walletAddress)); + } + else if (walletAddress.equalsIgnoreCase(contractAddress)) //transactions sent from or sent to the main currency account + { + return from.equalsIgnoreCase(walletAddress) || to.equalsIgnoreCase(walletAddress); + } + else if (to.equalsIgnoreCase(contractAddress)) + { + return true; + } + else + { + return getWalletInvolvedInTransaction(walletAddress); + } + } + + /** + * Fetch result of transaction operation. + * This is very much a WIP + * + * @param token + * @return + */ + public String getOperationResult(Token token, int precision) + { + //get amount here. will be amount + symbol if appropriate + if (hasInput()) + { + decodeTransactionInput(token.getWallet()); + String value = transactionInput.getOperationValue(token, this); + boolean isSendOrReceive = !from.equalsIgnoreCase(to) && transactionInput.isSendOrReceive(this); + String prefix = (value.length() == 0 || (value.startsWith("#") || !isSendOrReceive)) ? "" : + (token.getIsSent(this) ? "- " : "+ "); + return prefix + value; + } + else + { + return token.getTransactionValue(this, precision); + } + } + + /** + * Can the contract call be valid if the operation token is Ethereum? + * + * @param token + * @return + */ + public boolean shouldShowSymbol(Token token) + { + if (hasInput()) + { + decodeTransactionInput(token.getWallet()); + return transactionInput.shouldShowSymbol(token); + } + else + { + return true; + } + } + + public String getOperationTokenAddress() + { + if (hasInput()) + { + return to; + } + else + { + return ""; + } + } + + public boolean isLegacyTransaction() + { + try + { + return !TextUtils.isEmpty(gasPrice) && new BigInteger(gasPrice).compareTo(BigInteger.ZERO) > 0; + } + catch (Exception e) + { + return true; + } + } + + public String getOperationName(Context ctx, Token token, String walletAddress) + { + String txName = null; + if (isPending()) + { + txName = ctx.getString(R.string.status_pending); + } + else if (hasInput()) + { + decodeTransactionInput(walletAddress); + if (token.isEthereum() && shouldShowSymbol(token)) + { + transactionInput.type = TransactionType.CONTRACT_CALL; + } + + return transactionInput.getOperationTitle(ctx); + } + + return txName; + } + + public boolean hasInput() + { + return input != null && input.length() >= 10; + } + + public int getOperationToFrom(String walletAddress) + { + if (hasInput()) + { + decodeTransactionInput(walletAddress); + return transactionInput.getOperationToFrom(); + } + else + { + return 0; + } + } + + public StatusType getOperationImage(Token token) + { + if (hasError()) + { + return StatusType.FAILED; + } + else if (hasInput()) + { + decodeTransactionInput(token.getWallet()); + return transactionInput.getOperationImage(this, token.getWallet()); + } + else + { + return from.equalsIgnoreCase(token.getWallet()) ? StatusType.SENT : StatusType.RECEIVE; + } + } + + public TransactionType getTransactionType(String wallet) + { + if (hasError()) + { + return TransactionType.UNKNOWN; + } + else if (hasInput()) + { + decodeTransactionInput(wallet); + return transactionInput.type; + } + else + { + return TransactionType.SEND_ETH; + } + } + + /** + * Supplimental info in this case is the intrinsic root value attached to a contract call + * EG: Calling cryptokitties ERC721 'breedWithAuto' function requires you to call the function and also attach a small amount of ETH + * for the 'breeding fee'. That fee is later released to the caller of the 'birth' function. + * Supplemental info for these transaction would appear as -0.031 for the 'breedWithAuto' and +0.031 on the 'birth' call + * However it's not that simple - the 'breeding fee' will be in the value attached to the transaction, however the 'midwife' reward appears + * as an internal transaction, so won't be in the 'value' property. + * + * @return + */ + public String getSupplementalInfo(String walletAddress, String networkName) + { + if (hasInput()) + { + decodeTransactionInput(walletAddress); + return transactionInput.getSupplimentalInfo(this, walletAddress, networkName); + } + else + { + return ""; + } + } + + public String getPrefix(Token token) + { + if (hasInput()) + { + decodeTransactionInput(token.getWallet()); + if (!transactionInput.isSendOrReceive(this) || token.isEthereum()) + { + return ""; + } + else if (token.isERC721()) + { + return ""; + } + } + + boolean isSent = token.getIsSent(this); + boolean isSelf = from.equalsIgnoreCase(to); + if (isSelf) return ""; + else if (isSent) return "- "; + else return "+ "; + } public BigDecimal getRawValue(String walletAddress) throws Exception { - if (hasInput()) - { - decodeTransactionInput(walletAddress); - return transactionInput.getRawValue(); - } - else - { - return new BigDecimal(value); - } - } - - public StatusType getTransactionStatus() - { - if (hasError()) - { - return StatusType.FAILED; - } - else if (blockNumber.equals("-1")) - { - return StatusType.REJECTED; - } - else if (isPending()) - { - return StatusType.PENDING; - } - else - { - return null; - } - } + if (hasInput()) + { + decodeTransactionInput(walletAddress); + return transactionInput.getRawValue(); + } + else + { + return new BigDecimal(value); + } + } + + public StatusType getTransactionStatus() + { + if (hasError()) + { + return StatusType.FAILED; + } + else if (blockNumber.equals("-1")) + { + return StatusType.REJECTED; + } + else if (isPending()) + { + return StatusType.PENDING; + } + else + { + return null; + } + } public void addTransactionElements(Map resultMap) { - resultMap.put("__hash", new EventResult("", hash)); - resultMap.put("__to", new EventResult("", to)); - resultMap.put("__from", new EventResult("", from)); - resultMap.put("__value", new EventResult("", value)); - resultMap.put("__chainId", new EventResult("", String.valueOf(chainId))); - } - - public String getEventName(String walletAddress) - { - String eventName = ""; - if (hasInput()) - { - decodeTransactionInput(walletAddress); - eventName = transactionInput.getOperationEvent(walletAddress); - } - - return eventName; - } - - public int getSupplementalColour(String supplementalTxt) - { - if (!TextUtils.isEmpty(supplementalTxt)) - { - switch (supplementalTxt.charAt(1)) - { - case '-': - return R.color.negative; - case '+': - return R.color.positive; - default: - break; - } - } - - return R.color.text_primary; - } - - public String getDestination(Token token) - { - if (hasInput()) - { - decodeTransactionInput(token.getWallet()); - return transactionInput.getOperationAddress(this, token); - } - else - { - return token.getAddress(); - } - } - - public String getOperationDetail(Context ctx, Token token, TokensService tService) - { - if (hasInput()) - { - decodeTransactionInput(token.getWallet()); - return transactionInput.getOperationDescription (ctx, this, token, tService); - } - else - { - return ctx.getString(R.string.operation_definition, ctx.getString(R.string.to), ENSHandler.matchENSOrFormat(ctx, to)); - } - } - - private void decodeTransactionInput(String walletAddress) - { - if (transactionInput == null && hasInput() && Utils.isAddressValid(walletAddress)) - { - transactionInput = decoder.decodeInput(this, walletAddress); - } - } - - public boolean getWalletInvolvedInTransaction(String walletAddr) - { - decodeTransactionInput(walletAddr); - if ((transactionInput != null && transactionInput.functionData != null) && transactionInput.containsAddress(walletAddr)) return true; - else if (from.equalsIgnoreCase(walletAddr)) return true; - else if (to.equalsIgnoreCase(walletAddr)) return true; - else return input != null && input.length() > 40 && input.contains(Numeric.cleanHexPrefix(walletAddr.toLowerCase())); - } - - public boolean isNFTSent(String walletAddress) - { - if (hasInput()) - { - decodeTransactionInput(walletAddress); - return transactionInput.isSent(); - } - else - { - return true; - } - } - - public boolean getIsSent(String walletAddress) - { - if (hasInput()) - { - decodeTransactionInput(walletAddress); - return transactionInput.isSent(); - } - else - { - return from.equalsIgnoreCase(walletAddress); - } - } - - public boolean isValueChange(String walletAddress) - { - if (hasInput()) - { - decodeTransactionInput(walletAddress); - return transactionInput.isSendOrReceive(this); - } - else - { - return true; - } - } - - private String calculateContractAddress(String account, long nonce){ - byte[] addressAsBytes = Numeric.hexStringToByteArray(account); - byte[] calculatedAddressAsBytes = - Hash.sha3(RlpEncoder.encode( - new RlpList( - RlpString.create(addressAsBytes), - RlpString.create((nonce))))); - - calculatedAddressAsBytes = Arrays.copyOfRange(calculatedAddressAsBytes, - 12, calculatedAddressAsBytes.length); - return Numeric.toHexString(calculatedAddressAsBytes); - } + resultMap.put("__hash", new EventResult("", hash)); + resultMap.put("__to", new EventResult("", to)); + resultMap.put("__from", new EventResult("", from)); + resultMap.put("__value", new EventResult("", value)); + resultMap.put("__chainId", new EventResult("", String.valueOf(chainId))); + } + + public String getEventName(String walletAddress) + { + String eventName = ""; + if (hasInput()) + { + decodeTransactionInput(walletAddress); + eventName = transactionInput.getOperationEvent(walletAddress); + } + + return eventName; + } + + public int getSupplementalColour(String supplementalTxt) + { + if (!TextUtils.isEmpty(supplementalTxt)) + { + switch (supplementalTxt.charAt(1)) + { + case '-': + return R.color.negative; + case '+': + return R.color.positive; + default: + break; + } + } + + return R.color.text_primary; + } + + public String getDestination(Token token) + { + if (token == null) return ""; + if (hasInput()) + { + decodeTransactionInput(token.getWallet()); + return transactionInput.getOperationAddress(this, token); + } + else + { + return token.getAddress(); + } + } + + public String getOperationDetail(Context ctx, Token token, TokensService tService) + { + if (hasInput()) + { + decodeTransactionInput(token.getWallet()); + return transactionInput.getOperationDescription(ctx, this, token, tService); + } + else + { + return ctx.getString(R.string.operation_definition, ctx.getString(R.string.to), ENSHandler.matchENSOrFormat(ctx, to)); + } + } + + private void decodeTransactionInput(String walletAddress) + { + if (transactionInput == null && hasInput() && Utils.isAddressValid(walletAddress)) + { + transactionInput = decoder.decodeInput(this, walletAddress); + } + } + + public boolean getWalletInvolvedInTransaction(String walletAddr) + { + decodeTransactionInput(walletAddr); + if ((transactionInput != null && transactionInput.functionData != null) && transactionInput.containsAddress(walletAddr)) + return true; + else if (from.equalsIgnoreCase(walletAddr)) return true; + else if (to.equalsIgnoreCase(walletAddr)) return true; + else + return input != null && input.length() > 40 && input.contains(Numeric.cleanHexPrefix(walletAddr.toLowerCase())); + } + + public boolean isNFTSent(String walletAddress) + { + if (hasInput()) + { + decodeTransactionInput(walletAddress); + return transactionInput.isSent(); + } + else + { + return true; + } + } + + public boolean getIsSent(String walletAddress) + { + if (hasInput()) + { + decodeTransactionInput(walletAddress); + return transactionInput.isSent(); + } + else + { + return from.equalsIgnoreCase(walletAddress); + } + } + + public boolean isValueChange(String walletAddress) + { + if (hasInput()) + { + decodeTransactionInput(walletAddress); + return transactionInput.isSendOrReceive(this); + } + else + { + return true; + } + } + + private String calculateContractAddress(String account, long nonce) + { + byte[] addressAsBytes = Numeric.hexStringToByteArray(account); + byte[] calculatedAddressAsBytes = + Hash.sha3(RlpEncoder.encode( + new RlpList( + RlpString.create(addressAsBytes), + RlpString.create((nonce))))); + + calculatedAddressAsBytes = Arrays.copyOfRange(calculatedAddressAsBytes, + 12, calculatedAddressAsBytes.length); + return Numeric.toHexString(calculatedAddressAsBytes); + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java b/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java index 53bae51983..2a22edb640 100644 --- a/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java +++ b/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java @@ -1,6 +1,8 @@ package com.alphawallet.app.entity; -import com.alphawallet.app.BuildConfig; +import static com.alphawallet.app.entity.TransactionDecoder.ReadState.ARGS; +import static org.web3j.crypto.Keys.ADDRESS_LENGTH_IN_HEX; + import com.alphawallet.app.web3.entity.Web3Transaction; import org.web3j.crypto.Hash; @@ -13,9 +15,6 @@ import java.util.List; import java.util.Map; -import static com.alphawallet.app.entity.TransactionDecoder.ReadState.ARGS; -import static org.web3j.crypto.Keys.ADDRESS_LENGTH_IN_HEX; - import timber.log.Timber; /** @@ -29,7 +28,6 @@ * input. */ -// TODO: Should be a factory class that emits an object containing transaction interpretation public class TransactionDecoder { public static final int FUNCTION_LENGTH = 10; @@ -43,7 +41,7 @@ public class TransactionDecoder private FunctionData getUnknownFunction() { - return new FunctionData("N/A", ContractType.OTHER); + return new FunctionData("Contract Call", ContractType.OTHER); } public TransactionDecoder() @@ -63,9 +61,12 @@ public TransactionInput decodeInput(String input) return thisData; } - try { - while (parseIndex < input.length() && !(parseState == ParseStage.FINISH)) { - switch (parseState) { + try + { + while (parseIndex < input.length() && !(parseState == ParseStage.FINISH)) + { + switch (parseState) + { case PARSE_FUNCTION: //get function parseState = setFunction(thisData, readBytes(input, FUNCTION_LENGTH), input.length()); break; @@ -107,7 +108,8 @@ public TransactionInput decodeInput(Web3Transaction web3Tx, long chainId, String return thisData; } - private ParseStage setFunction(TransactionInput thisData, String input, int length) { + private ParseStage setFunction(TransactionInput thisData, String input, int length) + { //first get expected arg list: FunctionData data = functionList.get(input); @@ -136,7 +138,8 @@ enum ReadState SIGNATURE } - private ParseStage getParams(TransactionInput thisData, String input) { + private ParseStage getParams(TransactionInput thisData, String input) + { state = ARGS; BigInteger count; StringBuilder sb = new StringBuilder(); @@ -152,26 +155,33 @@ private ParseStage getParams(TransactionInput thisData, String input) { sb.setLength(0); argData = read256bits(input); BigInteger dataCount = Numeric.toBigInt(argData); - String stuff = readBytes(input, dataCount.intValue()); - thisData.miscData.add(stuff); + String hexBytes = readBytes(input, dataCount.intValue()); + thisData.miscData.add(hexBytes); + thisData.hexArgs.add("0x" + hexBytes); break; case "string": count = new BigInteger(argData, 16); sb.setLength(0); argData = read256bits(input); - if (count.intValue() > argData.length()) count = BigInteger.valueOf(argData.length()); - for (int index = 0; index < (count.intValue()*2); index += 2) + if (count.intValue() > argData.length()) + count = BigInteger.valueOf(argData.length()); + for (int index = 0; index < (count.intValue() * 2); index += 2) { - int v = Integer.parseInt(argData.substring(index, index+2), 16); - char c = (char)v; + int v = Integer.parseInt(argData.substring(index, index + 2), 16); + char c = (char) v; sb.append(c); } thisData.miscData.add(Numeric.cleanHexPrefix(sb.toString())); + + //Should be ASCII, try to convert + thisData.hexArgs.add(new String(Numeric.hexStringToByteArray(sb.toString()))); break; case "address": if (argData.length() >= 64 - ADDRESS_LENGTH_IN_HEX) { - thisData.addresses.add("0x" + argData.substring(64 - ADDRESS_LENGTH_IN_HEX)); + String addr = "0x" + argData.substring(64 - ADDRESS_LENGTH_IN_HEX); + thisData.addresses.add(addr); + thisData.hexArgs.add(addr); } break; case "bytes32": @@ -181,17 +191,21 @@ private ParseStage getParams(TransactionInput thisData, String input) { case "uint16[]": case "uint256[]": count = new BigInteger(argData, 16); - for (int i = 0; i < count.intValue(); i++) { + for (int i = 0; i < count.intValue(); i++) + { String inputData = read256bits(input); thisData.arrayValues.add(new BigInteger(inputData, 16)); + thisData.hexArgs.add(inputData); if (inputData.equals("0")) break; } break; case "uint256": + case "uint": addArg(thisData, argData); break; case "uint8": //In our standards, we will put uint8 as the signature marker - if (thisData.functionData.hasSig) { + if (thisData.functionData.hasSig) + { state = ReadState.SIGNATURE; sigCount = 0; } @@ -200,6 +214,11 @@ private ParseStage getParams(TransactionInput thisData, String input) { case "nodata": //no need to store this data - eg placeholder to indicate presence of a vararg break; + case "bool": + //zero or one? + BigInteger val = new BigInteger(argData, 16); + thisData.hexArgs.add(val.longValue() == 0 ? "false" : "true"); + break; default: break; } @@ -225,13 +244,14 @@ private void addArg(TransactionInput thisData, String input) if (++sigCount == 3) state = ARGS; break; } + thisData.hexArgs.add(input); } private String readBytes(String input, int bytes) { if ((parseIndex + bytes) <= input.length()) { - String value = input.substring(parseIndex, parseIndex+bytes); + String value = input.substring(parseIndex, parseIndex + bytes); parseIndex += bytes; return value; } @@ -245,7 +265,7 @@ private String read256bits(String input) { if ((parseIndex + 64) <= input.length()) { - String value = input.substring(parseIndex, parseIndex+64); + String value = input.substring(parseIndex, parseIndex + 64); parseIndex += 64; return value; } @@ -452,7 +472,7 @@ public int[] getIndices(TransactionInput data) if (data != null && data.arrayValues != null) { indices = new int[data.arrayValues.size()]; - for (int i = 0; i < data.arrayValues.size() ; i++) + for (int i = 0; i < data.arrayValues.size(); i++) { indices[i] = data.arrayValues.get(i).intValue(); } @@ -461,7 +481,8 @@ public int[] getIndices(TransactionInput data) return indices; } - public static String buildMethodId(String methodSignature) { + public static String buildMethodId(String methodSignature) + { byte[] input = methodSignature.getBytes(); byte[] hash = Hash.sha3(input); return Numeric.toHexString(hash).substring(0, 10); diff --git a/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java b/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java index 69eee3cdf3..03ecb925e3 100644 --- a/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java +++ b/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java @@ -1,5 +1,9 @@ package com.alphawallet.app.entity; +import static com.alphawallet.app.C.BURN_ADDRESS; +import static com.alphawallet.app.C.ETHER_DECIMALS; +import static com.alphawallet.app.ui.widget.holder.TransactionHolder.TRANSACTION_BALANCE_PRECISION; + import android.content.Context; import android.text.TextUtils; @@ -23,11 +27,6 @@ import java.util.ArrayList; import java.util.List; -import static com.alphawallet.app.C.BURN_ADDRESS; -import static com.alphawallet.app.C.ETHER_DECIMALS; -import static com.alphawallet.app.entity.tokenscript.TokenscriptFunction.ZERO_ADDRESS; -import static com.alphawallet.app.ui.widget.holder.TransactionHolder.TRANSACTION_BALANCE_PRECISION; - /** * Created by James on 4/03/2018. * @@ -47,6 +46,7 @@ public class TransactionInput public List arrayValues; public List sigData; public List miscData; + public List hexArgs; public String tradeAddress; public TransactionType type; @@ -59,6 +59,7 @@ public TransactionInput() addresses = new ArrayList<>(); sigData = new ArrayList<>(); miscData = new ArrayList<>(); + hexArgs = new ArrayList<>(); } //Addresses are in 256bit format @@ -815,4 +816,45 @@ public boolean isSendOrReceive(Transaction tx) return !tx.value.equals("0"); } } + + public String buildFunctionCallText() + { + StringBuilder sb = new StringBuilder(); + sb.append(functionData.functionName); + sb.append("("); + boolean firstArg = true; + for (String arg : hexArgs) + { + if (!firstArg) sb.append(", "); + if (arg.startsWith("0")) + { + sb.append(truncateValue(arg)); + } + else + { + sb.append(arg); + } + firstArg = false; + } + + sb.append(")"); + + return sb.toString(); + } + + private String truncateValue(String arg) + { + String retVal = arg; + try + { + BigInteger argVal = new BigInteger(arg, 16); + retVal = argVal.toString(16); + } + catch (Exception e) + { + // + } + + return retVal; + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/Version.java b/app/src/main/java/com/alphawallet/app/entity/Version.java new file mode 100644 index 0000000000..b2f957d80f --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/Version.java @@ -0,0 +1,54 @@ +package com.alphawallet.app.entity; + +public class Version implements Comparable +{ + private final String version; + + public Version(String version) + { + if (version == null) + throw new IllegalArgumentException("Version cannot be null"); + if (!version.matches("[0-9]+(\\.[0-9]+)*")) + throw new IllegalArgumentException("Invalid version format"); + this.version = version; + } + + public final String get() + { + return this.version; + } + + @Override + public int compareTo(Version that) + { + if (that == null) + return 1; + String[] thisParts = this.get().split("\\."); + String[] thatParts = that.get().split("\\."); + int length = Math.max(thisParts.length, thatParts.length); + for (int i = 0; i < length; i++) + { + int thisPart = i < thisParts.length ? + Integer.parseInt(thisParts[i]) : 0; + int thatPart = i < thatParts.length ? + Integer.parseInt(thatParts[i]) : 0; + if (thisPart < thatPart) + return -1; + if (thisPart > thatPart) + return 1; + } + return 0; + } + + @Override + public boolean equals(Object that) + { + if (this == that) + return true; + if (that == null) + return false; + if (this.getClass() != that.getClass()) + return false; + return this.compareTo((Version) that) == 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/Wallet.java b/app/src/main/java/com/alphawallet/app/entity/Wallet.java index 799c3f499c..8d4a5d4d1f 100644 --- a/app/src/main/java/com/alphawallet/app/entity/Wallet.java +++ b/app/src/main/java/com/alphawallet/app/entity/Wallet.java @@ -10,7 +10,8 @@ import java.math.BigDecimal; -public class Wallet implements Parcelable { +public class Wallet implements Parcelable +{ public final String address; public String balance; public String ENSname; @@ -22,103 +23,115 @@ public class Wallet implements Parcelable { public String balanceSymbol; public String ENSAvatar; public boolean isSynced; + public Token[] tokens; - public Wallet(String address) { - this.address = address; - this.balance = "-"; - this.ENSname = ""; - this.name = ""; - this.type = WalletType.NOT_DEFINED; - this.lastBackupTime = 0; - this.authLevel = KeyService.AuthenticationLevel.NOT_SET; - this.walletCreationTime = 0; - this.balanceSymbol = ""; - this.ENSAvatar = ""; - } - - private Wallet(Parcel in) - { - address = in.readString(); - balance = in.readString(); - ENSname = in.readString(); - name = in.readString(); - int t = in.readInt(); - type = WalletType.values()[t]; - lastBackupTime = in.readLong(); - t = in.readInt(); - authLevel = KeyService.AuthenticationLevel.values()[t]; - walletCreationTime = in.readLong(); - balanceSymbol = in.readString(); - ENSAvatar = in.readString(); - } - - public void setWalletType(WalletType wType) - { - type = wType; - } - - public static final Creator CREATOR = new Creator() { - @Override - public Wallet createFromParcel(Parcel in) { - return new Wallet(in); - } - - @Override - public Wallet[] newArray(int size) { - return new Wallet[size]; - } - }; - - public boolean sameAddress(String address) { - return this.address.equalsIgnoreCase(address); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int i) - { - parcel.writeString(address); - parcel.writeString(balance); - parcel.writeString(ENSname); - parcel.writeString(name); - parcel.writeInt(type.ordinal()); - parcel.writeLong(lastBackupTime); - parcel.writeInt(authLevel.ordinal()); - parcel.writeLong(walletCreationTime); - parcel.writeString(balanceSymbol); - parcel.writeString(ENSAvatar); - } - - public boolean setWalletBalance(Token token) - { - balanceSymbol = token.tokenInfo != null ? token.tokenInfo.symbol : "ETH"; - String newBalance = token.getFixedFormattedBalance(); - if (newBalance.equals(balance)) - { - return false; - } - else - { - balance = newBalance; - return true; - } - } - - public void zeroWalletBalance(NetworkInfo networkInfo) - { - if (balance.equals("-")) - { - balanceSymbol = networkInfo.symbol; - balance = BalanceUtils.getScaledValueFixed(BigDecimal.ZERO, 0, Token.TOKEN_BALANCE_PRECISION); - } - } + public Wallet(String address) + { + this.address = address; + this.balance = "-"; + this.ENSname = ""; + this.name = ""; + this.type = WalletType.NOT_DEFINED; + this.lastBackupTime = 0; + this.authLevel = KeyService.AuthenticationLevel.NOT_SET; + this.walletCreationTime = 0; + this.balanceSymbol = ""; + this.ENSAvatar = ""; + } + + private Wallet(Parcel in) + { + address = in.readString(); + balance = in.readString(); + ENSname = in.readString(); + name = in.readString(); + int t = in.readInt(); + type = WalletType.values()[t]; + lastBackupTime = in.readLong(); + t = in.readInt(); + authLevel = KeyService.AuthenticationLevel.values()[t]; + walletCreationTime = in.readLong(); + balanceSymbol = in.readString(); + ENSAvatar = in.readString(); + } + + public void setWalletType(WalletType wType) + { + type = wType; + } + + public static final Creator CREATOR = new Creator() + { + @Override + public Wallet createFromParcel(Parcel in) + { + return new Wallet(in); + } + + @Override + public Wallet[] newArray(int size) + { + return new Wallet[size]; + } + }; + + public boolean sameAddress(String address) + { + return this.address.equalsIgnoreCase(address); + } + + @Override + public int describeContents() + { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) + { + parcel.writeString(address); + parcel.writeString(balance); + parcel.writeString(ENSname); + parcel.writeString(name); + parcel.writeInt(type.ordinal()); + parcel.writeLong(lastBackupTime); + parcel.writeInt(authLevel.ordinal()); + parcel.writeLong(walletCreationTime); + parcel.writeString(balanceSymbol); + parcel.writeString(ENSAvatar); + } + + public boolean setWalletBalance(Token token) + { + balanceSymbol = token.tokenInfo != null ? token.tokenInfo.symbol : "ETH"; + String newBalance = token.getFixedFormattedBalance(); + if (newBalance.equals(balance)) + { + return false; + } + else + { + balance = newBalance; + return true; + } + } + + public void zeroWalletBalance(NetworkInfo networkInfo) + { + if (balance.equals("-")) + { + balanceSymbol = networkInfo.symbol; + balance = BalanceUtils.getScaledValueFixed(BigDecimal.ZERO, 0, Token.TOKEN_BALANCE_PRECISION); + } + } public boolean canSign() { - return BuildConfig.DEBUG || type != WalletType.WATCH; + return BuildConfig.DEBUG || !watchOnly(); + } + + public boolean watchOnly() + { + return type == WalletType.WATCH; } } diff --git a/app/src/main/java/com/alphawallet/app/entity/analytics/ActionSheetMode.java b/app/src/main/java/com/alphawallet/app/entity/analytics/ActionSheetMode.java new file mode 100644 index 0000000000..41376a1e60 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/analytics/ActionSheetMode.java @@ -0,0 +1,30 @@ +package com.alphawallet.app.entity.analytics; + +/** + * Created by JB on 12/01/2021. + */ +public enum ActionSheetMode +{ + SEND_TRANSACTION("Send Transaction"), + SEND_TRANSACTION_DAPP("Send Transaction DApp"), + SEND_TRANSACTION_WC("Send Transaction WalletConnect"), + SIGN_MESSAGE("Sign Message"), + SIGN_TRANSACTION("Sign Transaction"), + SPEEDUP_TRANSACTION("Speed Up Transaction"), + CANCEL_TRANSACTION("Cancel Transaction"), + MESSAGE("Message"), + WALLET_CONNECT_REQUEST("WalletConnect Request"), + NODE_STATUS_INFO("Node Status Info"); + + private final String mode; + + ActionSheetMode(String mode) + { + this.mode = mode; + } + + public String getValue() + { + return mode; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/analytics/ActionSheetSource.java b/app/src/main/java/com/alphawallet/app/entity/analytics/ActionSheetSource.java new file mode 100644 index 0000000000..05a9b9e92b --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/analytics/ActionSheetSource.java @@ -0,0 +1,26 @@ +package com.alphawallet.app.entity.analytics; + +public enum ActionSheetSource +{ + WALLET_CONNECT("WalletConnect"), + SWAP("Swap"), + SEND_FUNGIBLE("Send Fungible"), + SEND_NFT("Send NFT"), + TOKENSCRIPT("TokenScript"), + BROWSER("Browser"), + CLAIM_PAID_MAGIC_LINK("Claim Paid MagicLink"), + SPEEDUP_TRANSACTION("Speed Up Transaction"), + CANCEL_TRANSACTION("Cancel Transaction"); + + private final String source; + + ActionSheetSource(String source) + { + this.source = source; + } + + public String getValue() + { + return source; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/analytics/FirstWalletAction.java b/app/src/main/java/com/alphawallet/app/entity/analytics/FirstWalletAction.java new file mode 100644 index 0000000000..21ad0d7c3a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/analytics/FirstWalletAction.java @@ -0,0 +1,21 @@ +package com.alphawallet.app.entity.analytics; + +public enum FirstWalletAction +{ + CREATE_WALLET("Create Wallet"), + IMPORT_WALLET("Import Wallet"); + + public static final String KEY = "action"; + + private final String action; + + FirstWalletAction(String action) + { + this.action = action; + } + + public String getValue() + { + return action; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/analytics/ImportWalletType.java b/app/src/main/java/com/alphawallet/app/entity/analytics/ImportWalletType.java new file mode 100644 index 0000000000..707ba22ec2 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/analytics/ImportWalletType.java @@ -0,0 +1,21 @@ +package com.alphawallet.app.entity.analytics; + +public enum ImportWalletType +{ + SEED_PHRASE("Seed Phrase"), + KEYSTORE("Keystore"), + PRIVATE_KEY("Private Key"), + WATCH("Watch"); + + private final String type; + + ImportWalletType(String type) + { + this.type = type; + } + + public String getValue() + { + return type; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/analytics/QrScanSource.java b/app/src/main/java/com/alphawallet/app/entity/analytics/QrScanSource.java new file mode 100644 index 0000000000..10cd9301c5 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/analytics/QrScanSource.java @@ -0,0 +1,27 @@ +package com.alphawallet.app.entity.analytics; + +public enum QrScanSource +{ + WALLET_CONNECT("Wallet Connect"), + ADDRESS_TEXT_FIELD("Address Text Field"), + BROWSER_SCREEN("Browser Screen"), + IMPORT_WALLET_SCREEN("Import Wallet Screen"), + ADD_CUSTOM_TOKEN_SCREEN("Add Custom Token Screen"), + WALLET_SCREEN("Wallet Screen"), + SEND_FUNGIBLE_SCREEN("Send Screen"), + QUICK_ACTION("Quick Action"); + + public static final String KEY = "qr_scan_source"; + + private final String type; + + QrScanSource(String type) + { + this.type = type; + } + + public String getValue() + { + return type; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/coinbasepay/DestinationWallet.java b/app/src/main/java/com/alphawallet/app/entity/coinbasepay/DestinationWallet.java new file mode 100644 index 0000000000..44ef08699c --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/coinbasepay/DestinationWallet.java @@ -0,0 +1,31 @@ +package com.alphawallet.app.entity.coinbasepay; + +import java.util.List; + +public class DestinationWallet +{ + final transient Type type; + String address; + List blockchains; + List assets; + + public DestinationWallet(Type type, String address, List list) + { + this.type = type; + this.address = address; + if (type.equals(Type.ASSETS)) + { + this.assets = list; + } + else + { + this.blockchains = list; + } + } + + public enum Type + { + ASSETS, + BLOCKCHAINS + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/Action.java b/app/src/main/java/com/alphawallet/app/entity/lifi/Action.java new file mode 100644 index 0000000000..110be10c1a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/Action.java @@ -0,0 +1,50 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class Action +{ + @SerializedName("fromChainId") + @Expose + public long fromChainId; + + @SerializedName("toChainId") + @Expose + public long toChainId; + + @SerializedName("fromToken") + @Expose + public Token fromToken; + + @SerializedName("toToken") + @Expose + public Token toToken; + + @SerializedName("fromAmount") + @Expose + public String fromAmount; + + @SerializedName("slippage") + @Expose + public double slippage; + + @SerializedName("fromAddress") + @Expose + public String fromAddress; + + @SerializedName("toAddress") + @Expose + public String toAddress; + + public String getCurrentPrice() + { + return new BigDecimal(fromToken.priceUSD) + .divide(new BigDecimal(toToken.priceUSD), 4, RoundingMode.DOWN) + .stripTrailingZeros() + .toPlainString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/Connection.java b/app/src/main/java/com/alphawallet/app/entity/lifi/Connection.java index cc1f084bcd..01fe4a324c 100644 --- a/app/src/main/java/com/alphawallet/app/entity/lifi/Connection.java +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/Connection.java @@ -4,7 +4,6 @@ import com.google.gson.annotations.SerializedName; import java.util.List; -import java.util.Objects; public class Connection { @@ -18,61 +17,9 @@ public class Connection @SerializedName("fromTokens") @Expose - public List fromTokens; + public List fromTokens; @SerializedName("toTokens") @Expose - public List toTokens; - - public static class LToken - { - @SerializedName("address") - @Expose - public String address; - - @SerializedName("symbol") - @Expose - public String symbol; - - @SerializedName("decimals") - @Expose - public long decimals; - - @SerializedName("chainId") - @Expose - public long chainId; - - @SerializedName("name") - @Expose - public String name; - - @SerializedName("coinKey") - @Expose - public String coinKey; - - @SerializedName("priceUSD") - @Expose - public String priceUSD; - - @SerializedName("logoURI") - @Expose - public String logoURI; - - public String balance; - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - LToken lToken = (LToken) o; - return address.equals(lToken.address) && symbol.equals(lToken.symbol); - } - - @Override - public int hashCode() - { - return Objects.hash(address, symbol); - } - } + public List toTokens; } diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/Estimate.java b/app/src/main/java/com/alphawallet/app/entity/lifi/Estimate.java new file mode 100644 index 0000000000..ab63b81844 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/Estimate.java @@ -0,0 +1,100 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; + +public class Estimate +{ + @SerializedName("fromAmount") + @Expose + public String fromAmount; + + @SerializedName("toAmount") + @Expose + public String toAmount; + + @SerializedName("toAmountMin") + @Expose + public String toAmountMin; + + @SerializedName("approvalAddress") + @Expose + public String approvalAddress; + + @SerializedName("executionDuration") + @Expose + public long executionDuration; + + @SerializedName("feeCosts") + @Expose + public ArrayList feeCosts; + + @SerializedName("gasCosts") + @Expose + public ArrayList gasCosts; + + @SerializedName("data") + @Expose + public Data data; + + @SerializedName("fromAmountUSD") + @Expose + public String fromAmountUSD; + + @SerializedName("toAmountUSD") + @Expose + public String toAmountUSD; + + public static class Data + { + @SerializedName("blockNumber") + @Expose + public long blockNumber; + + @SerializedName("network") + @Expose + public long network; + + @SerializedName("srcToken") + @Expose + public String srcToken; + + @SerializedName("srcDecimals") + @Expose + public long srcDecimals; + + @SerializedName("srcAmount") + @Expose + public String srcAmount; + + @SerializedName("destToken") + @Expose + public String destToken; + + @SerializedName("destDecimals") + @Expose + public long destDecimals; + + @SerializedName("destAmount") + @Expose + public String destAmount; + + @SerializedName("gasCostUSD") + @Expose + public String gasCostUSD; + + @SerializedName("gasCost") + @Expose + public String gasCost; + + @SerializedName("buyAmount") + @Expose + public String buyAmount; + + @SerializedName("sellAmount") + @Expose + public String sellAmount; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/FeeCost.java b/app/src/main/java/com/alphawallet/app/entity/lifi/FeeCost.java new file mode 100644 index 0000000000..bbbd5cd645 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/FeeCost.java @@ -0,0 +1,25 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; + +public class FeeCost +{ + @SerializedName("name") + @Expose + public String name; + + @SerializedName("percentage") + @Expose + public String percentage; + + @SerializedName("token") + @Expose + public Token token; + + @SerializedName("amount") + @Expose + public String amount; +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/GasCost.java b/app/src/main/java/com/alphawallet/app/entity/lifi/GasCost.java new file mode 100644 index 0000000000..d2ccd5b39b --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/GasCost.java @@ -0,0 +1,19 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GasCost +{ + @SerializedName("amount") + @Expose + public String amount; + + @SerializedName("amountUSD") + @Expose + public String amountUSD; + + @SerializedName("token") + @Expose + public Token token; +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/Quote.java b/app/src/main/java/com/alphawallet/app/entity/lifi/Quote.java index 7d7dd6bbc5..fc2d80e4f1 100644 --- a/app/src/main/java/com/alphawallet/app/entity/lifi/Quote.java +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/Quote.java @@ -3,9 +3,6 @@ import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; -import org.json.JSONArray; -import org.json.JSONObject; - public class Quote { @SerializedName("id") @@ -20,143 +17,18 @@ public class Quote @Expose public String tool; + @SerializedName("toolDetails") + @Expose + public SwapProvider swapProvider; + @SerializedName("action") @Expose public Action action; - public static class Action - { - @SerializedName("fromChainId") - @Expose - public long fromChainId; - - @SerializedName("toChainId") - @Expose - public long toChainId; - - @SerializedName("fromToken") - @Expose - public Connection.LToken fromToken; - - @SerializedName("toToken") - @Expose - public Connection.LToken toToken; - - @SerializedName("fromAmount") - @Expose - public String fromAmount; - - @SerializedName("slippage") - @Expose - public double slippage; - - @SerializedName("fromAddress") - @Expose - public String fromAddress; - - @SerializedName("toAddress") - @Expose - public String toAddress; - } - @SerializedName("estimate") @Expose public Estimate estimate; - public static class Estimate - { - @SerializedName("fromAmount") - @Expose - public String fromAmount; - - @SerializedName("toAmount") - @Expose - public String toAmount; - - @SerializedName("toAmountMin") - @Expose - public String toAmountMin; - - @SerializedName("approvalAddress") - @Expose - public String approvalAddress; - - @SerializedName("executionDuration") - @Expose - public long executionDuration; - -// @SerializedName("feeCosts") -// @Expose -// public JSONArray feeCosts; -// -// @SerializedName("gasCosts") -// @Expose -// public JSONArray gasCosts; - - @SerializedName("data") - @Expose - public Data data; - - public static class Data - { - @SerializedName("blockNumber") - @Expose - public long blockNumber; - - @SerializedName("network") - @Expose - public long network; - - @SerializedName("srcToken") - @Expose - public String srcToken; - - @SerializedName("srcDecimals") - @Expose - public long srcDecimals; - - @SerializedName("srcAmount") - @Expose - public String srcAmount; - - @SerializedName("destToken") - @Expose - public String destToken; - - @SerializedName("destDecimals") - @Expose - public long destDecimals; - - @SerializedName("destAmount") - @Expose - public String destAmount; - - @SerializedName("gasCostUSD") - @Expose - public String gasCostUSD; - - @SerializedName("gasCost") - @Expose - public String gasCost; - - @SerializedName("buyAmount") - @Expose - public String buyAmount; - - @SerializedName("sellAmount") - @Expose - public String sellAmount; - } - - @SerializedName("fromAmountUSD") - @Expose - public String fromAmountUSD; - - @SerializedName("toAmountUSD") - @Expose - public String toAmountUSD; - } - @SerializedName("transactionRequest") @Expose public TransactionRequest transactionRequest; diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/Route.java b/app/src/main/java/com/alphawallet/app/entity/lifi/Route.java new file mode 100644 index 0000000000..97719ea00c --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/Route.java @@ -0,0 +1,36 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class Route +{ + @SerializedName("gasCostUSD") + @Expose + public String gasCostUSD; + + @SerializedName("steps") + @Expose + public List steps; + + @SerializedName("tags") + @Expose + public List tags; + + public static class Step + { + @SerializedName("toolDetails") + @Expose + public SwapProvider swapProvider; + + @SerializedName("action") + @Expose + public Action action; + + @SerializedName("estimate") + @Expose + public Estimate estimate; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/RouteError.java b/app/src/main/java/com/alphawallet/app/entity/lifi/RouteError.java new file mode 100644 index 0000000000..aa6135d733 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/RouteError.java @@ -0,0 +1,23 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class RouteError +{ + @SerializedName("tool") + @Expose + public String tool; + + @SerializedName("message") + @Expose + public String message; + + @SerializedName("errorType") + @Expose + public String errorType; + + @SerializedName("code") + @Expose + public String code; +} diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/RouteOptions.java b/app/src/main/java/com/alphawallet/app/entity/lifi/RouteOptions.java new file mode 100644 index 0000000000..99882f661a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/RouteOptions.java @@ -0,0 +1,33 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.Gson; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class RouteOptions +{ + public String integrator; + public String slippage; + public Exchanges exchanges; + public String order; + + public RouteOptions() + { + this.exchanges = new Exchanges(); + } + + public static class Exchanges + { + public List allow = new ArrayList<>(); + } + + public JSONObject getJson() throws JSONException + { + String json = new Gson().toJson(this); + return new JSONObject(json); + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/SwapProvider.java b/app/src/main/java/com/alphawallet/app/entity/lifi/SwapProvider.java new file mode 100644 index 0000000000..6b01e6a292 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/SwapProvider.java @@ -0,0 +1,25 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class SwapProvider +{ + @SerializedName("key") + @Expose + public String key; + + @SerializedName("name") + @Expose + public String name; + + @SerializedName("logoURI") + @Expose + public String logoURI; + + @SerializedName("url") + @Expose + public String url; + + public boolean isChecked; +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/lifi/Token.java b/app/src/main/java/com/alphawallet/app/entity/lifi/Token.java new file mode 100644 index 0000000000..83ec1225a6 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/lifi/Token.java @@ -0,0 +1,91 @@ +package com.alphawallet.app.entity.lifi; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.Objects; + +public class Token +{ + @SerializedName("address") + @Expose + public String address; + + @SerializedName("symbol") + @Expose + public String symbol; + + @SerializedName("decimals") + @Expose + public long decimals; + + @SerializedName("chainId") + @Expose + public long chainId; + + @SerializedName("name") + @Expose + public String name; + + @SerializedName("coinKey") + @Expose + public String coinKey; + + @SerializedName("priceUSD") + @Expose + public String priceUSD; + + @SerializedName("logoURI") + @Expose + public String logoURI; + + public String balance; + public double fiatEquivalent; + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Token lToken = (Token) o; + return address.equals(lToken.address) && symbol.equals(lToken.symbol); + } + + @Override + public int hashCode() + { + return Objects.hash(address, symbol); + } + + // Note: In the LIFI API, the native token has either of these two addresses. + public boolean isNativeToken() + { + return address.equalsIgnoreCase("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") || + address.equalsIgnoreCase("0x0000000000000000000000000000000000000000"); + } + + public double getFiatValue() + { + try + { + double value = Double.parseDouble(balance); + double priceUSD = Double.parseDouble(this.priceUSD); + return value * priceUSD; + } + catch (NumberFormatException | NullPointerException e) + { + return 0.0; + } + } + + public boolean isSimilarTo(com.alphawallet.app.entity.tokens.Token aToken, String walletAddress) + { + if (this.chainId == aToken.tokenInfo.chainId + && this.address.equalsIgnoreCase(aToken.getAddress())) + { + return true; + } + + return aToken.getAddress().equalsIgnoreCase(walletAddress) && isNativeToken(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java index b461cb184a..1a9b46ee76 100644 --- a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java +++ b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java @@ -55,8 +55,8 @@ public NFTAsset[] newArray(int size) private static final String DESCRIPTION = "description"; private static final String IMAGE_ORIGINAL_URL = "image_original_url"; private static final String IMAGE_ANIMATION = "animation_url"; - private static final String[] IMAGE_DESIGNATORS = {IMAGE, IMAGE_URL, IMAGE_ANIMATION, IMAGE_ORIGINAL_URL, IMAGE_PREVIEW}; - private static final String[] SVG_OVERRIDE = {IMAGE_ORIGINAL_URL, IMAGE_ANIMATION, IMAGE, IMAGE_URL}; + private static final String[] IMAGE_DESIGNATORS = {IMAGE, IMAGE_URL, IMAGE_ORIGINAL_URL, IMAGE_PREVIEW, IMAGE_ANIMATION}; + private static final String[] SVG_OVERRIDE = {IMAGE_ORIGINAL_URL, IMAGE, IMAGE_URL, IMAGE_ANIMATION}; private static final String[] IMAGE_THUMBNAIL_DESIGNATORS = {IMAGE_PREVIEW, IMAGE, IMAGE_URL, IMAGE_ORIGINAL_URL, IMAGE_ANIMATION}; private static final String BACKGROUND_COLOUR = "background_color"; private static final String EXTERNAL_LINK = "external_link"; @@ -81,7 +81,8 @@ public NFTAsset(String metaData) public NFTAsset(RealmNFTAsset realmAsset) { - loadFromMetaData(realmAsset.getMetaData()); + String metaData = realmAsset.getMetaData() != null ? realmAsset.getMetaData() : new NFTAsset(new BigInteger(realmAsset.getTokenId())).jsonMetaData(); + loadFromMetaData(metaData); balance = realmAsset.getBalance(); } @@ -99,6 +100,7 @@ public NFTAsset(BigInteger tokenId) attributeMap.clear(); balance = BigDecimal.ONE; assetMap.put(NAME, "ID #" + tokenId.toString()); + assetMap.put(LOADING_TOKEN, "."); } public NFTAsset(NFTAsset asset) @@ -169,10 +171,9 @@ public String getName() return assetMap.get(NAME); } - public boolean isAnimation() + public String getAnimation() { - String anim = assetMap.get(IMAGE_ANIMATION); - return anim != null; + return assetMap.get(IMAGE_ANIMATION); } public String getImage() @@ -382,6 +383,11 @@ public boolean needsLoading() return (assetMap.size() == 0 || assetMap.containsKey(LOADING_TOKEN)); } + public boolean hasImageAsset() + { + return !TextUtils.isEmpty(getThumbnail()); + } + public boolean requiresReplacement() { return (needsLoading() || !assetMap.containsKey(NAME) || TextUtils.isEmpty(getImage())); @@ -563,4 +569,4 @@ public String getValue() return this.category; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/entity/opensea/OpenSeaAsset.java b/app/src/main/java/com/alphawallet/app/entity/opensea/OpenSeaAsset.java index 9280fb2109..635de75260 100644 --- a/app/src/main/java/com/alphawallet/app/entity/opensea/OpenSeaAsset.java +++ b/app/src/main/java/com/alphawallet/app/entity/opensea/OpenSeaAsset.java @@ -40,6 +40,10 @@ public class OpenSeaAsset @Expose public String animationUrl; + @SerializedName("animation_url") + @Expose + public String animation_url; + @SerializedName("name") @Expose public String name; @@ -84,6 +88,10 @@ public class OpenSeaAsset @Expose public LastSale lastSale; + @SerializedName("rarity_data") + @Expose + public Rarity rarity; + public static class Collection { @SerializedName("stats") @@ -323,6 +331,22 @@ else if (totalPrice.length() <= decimals) return result; } + public String getAnimationUrl() + { + if (animationUrl != null) + { + return animationUrl; + } + else if (animation_url != null) + { + return animation_url; + } + else + { + return null; + } + } + public String getImageUrl() { if (image != null) @@ -345,6 +369,10 @@ else if (imagePreviewUrl != null) { return imagePreviewUrl; } + else if (animation_url != null) + { + return animation_url; + } else { return ""; diff --git a/app/src/main/java/com/alphawallet/app/entity/opensea/Rarity.java b/app/src/main/java/com/alphawallet/app/entity/opensea/Rarity.java new file mode 100644 index 0000000000..5160fcaec5 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/opensea/Rarity.java @@ -0,0 +1,31 @@ +package com.alphawallet.app.entity.opensea; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Rarity +{ + @SerializedName("strategy_id") + @Expose + public String strategyId; + + @SerializedName("strategy_version") + @Expose + public String strategyVersion; + + @SerializedName("rank") + @Expose + public long rank; + + @SerializedName("score") + @Expose + public double score; + + @SerializedName("max_rank") + @Expose + public long maxRank; + + @SerializedName("tokens_scored") + @Expose + public long tokensScored; +} diff --git a/app/src/main/java/com/alphawallet/app/entity/tokendata/TokenUpdateType.java b/app/src/main/java/com/alphawallet/app/entity/tokendata/TokenUpdateType.java new file mode 100644 index 0000000000..4f75f70e96 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/tokendata/TokenUpdateType.java @@ -0,0 +1,9 @@ +package com.alphawallet.app.entity.tokendata; + +/** + * Created by JB on 22/08/2022. + */ +public enum TokenUpdateType +{ + ACTIVE_SYNC, STORED +} diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java index f879983a38..84f8ecc789 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java @@ -276,27 +276,39 @@ public Function getTransferFunction(String to, List tokenIds) throws } @Override - public List getChangeList(Map assetMap) + public Map getAssetChange(Map oldAssetList) { - //detect asset removal - List oldAssetIdList = new ArrayList<>(assetMap.keySet()); - oldAssetIdList.removeAll(assets.keySet()); + //first see if there's no change; if this is the case we can skip + if (assetsUnchanged(oldAssetList)) return assets; - List changeList = new ArrayList<>(oldAssetIdList); + //add all known tokens in + Map sum = new HashMap<>(oldAssetList); + sum.putAll(assets); + Set tokenIds = sum.keySet(); + Function balanceOfBatch = balanceOfBatch(getWallet(), tokenIds); + List balances = callSmartContractFunctionArray(tokenInfo.chainId, balanceOfBatch, getAddress(), getWallet()); + Map updatedAssetMap; - //Now detect differences or new tokens - for (BigInteger tokenId : assets.keySet()) + if (balances != null && balances.size() > 0) { - NFTAsset newAsset = assets.get(tokenId); - NFTAsset oldAsset = assetMap.get(tokenId); - - if (oldAsset == null || newAsset.hashCode() != oldAsset.hashCode()) + updatedAssetMap = new HashMap<>(); + int index = 0; + for (BigInteger tokenId : tokenIds) { - changeList.add(tokenId); + NFTAsset thisAsset = new NFTAsset(sum.get(tokenId)); + BigInteger balance = balances.get(index).getValue(); + thisAsset.setBalance(new BigDecimal(balance)); + updatedAssetMap.put(tokenId, thisAsset); + + index++; } } + else + { + updatedAssetMap = assets; + } - return changeList; + return updatedAssetMap; } private List fetchBalances(Set tokenIds) @@ -308,16 +320,10 @@ private List fetchBalances(Set tokenIds) @Override public Map queryAssets(Map assetMap) { - //first see if there's no change; if this is the case we can skip - if (assetsUnchanged(assetMap)) return assets; - - //add all known tokens in - Map sum = new HashMap<>(assetMap); - sum.putAll(assets); - Set tokenIds = sum.keySet(); + Set tokenIds = assetMap.keySet(); Function balanceOfBatch = balanceOfBatch(getWallet(), tokenIds); List balances = callSmartContractFunctionArray(tokenInfo.chainId, balanceOfBatch, getAddress(), getWallet()); - Map updatedAssetMap; + Map updatedAssetMap = new HashMap<>(); if (balances != null && balances.size() > 0) { @@ -325,7 +331,7 @@ public Map queryAssets(Map assetMap) int index = 0; for (BigInteger tokenId : tokenIds) { - NFTAsset thisAsset = new NFTAsset(sum.get(tokenId)); + NFTAsset thisAsset = new NFTAsset(assetMap.get(tokenId)); BigInteger balance = balances.get(index).getValue(); thisAsset.setBalance(new BigDecimal(balance)); updatedAssetMap.put(tokenId, thisAsset); @@ -333,10 +339,6 @@ public Map queryAssets(Map assetMap) index++; } } - else - { - updatedAssetMap = assets; - } return updatedAssetMap; } @@ -643,7 +645,7 @@ public BigDecimal updateBalance(Realm realm) try { - final Web3j web3j = TokenRepository.getWeb3jService(tokenInfo.chainId); + final Web3j web3j = TokenRepository.getWeb3jServiceForEvents(tokenInfo.chainId); Pair, HashSet>> evRead = eventSync.processTransferEvents(web3j, getBalanceUpdateEvents(), startBlock, endBlock, realm); diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Ticket.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Ticket.java index 813eeda7fa..49a6e8c5ca 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Ticket.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Ticket.java @@ -171,7 +171,7 @@ public boolean hasArrayBalance() public List getNonZeroArrayBalance() { List nonZeroValues = new ArrayList<>(); - for (BigInteger value : balanceArray) if (value.compareTo(BigInteger.ZERO) != 0 && !nonZeroValues.contains(value)) nonZeroValues.add(value); + for (BigInteger value : balanceArray) if (value.compareTo(BigInteger.ZERO) != 0) nonZeroValues.add(value); return nonZeroValues; } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java index 6a3ad167cc..0de075182e 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java @@ -1,10 +1,13 @@ package com.alphawallet.app.entity.tokens; +import static com.alphawallet.app.repository.TokenRepository.callSmartContractFunction; +import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; import static com.alphawallet.app.util.Utils.parseTokenId; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; import static org.web3j.tx.Contract.staticExtractEventParameters; import android.app.Activity; +import android.text.TextUtils; import android.util.Pair; import com.alphawallet.app.R; @@ -15,6 +18,7 @@ import com.alphawallet.app.entity.TransactionInput; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokendata.TokenGroup; +import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EventResult; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.repository.entity.RealmNFTAsset; @@ -28,32 +32,41 @@ import org.web3j.abi.FunctionReturnDecoder; import org.web3j.abi.TypeEncoder; import org.web3j.abi.TypeReference; +import org.web3j.abi.Utils; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Event; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.BatchRequest; +import org.web3j.protocol.core.BatchResponse; import org.web3j.protocol.core.DefaultBlockParameter; import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.Response; import org.web3j.protocol.core.methods.request.EthFilter; import org.web3j.protocol.core.methods.response.EthCall; import org.web3j.protocol.core.methods.response.EthLog; import org.web3j.protocol.core.methods.response.Log; import org.web3j.utils.Numeric; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import io.realm.Case; import io.realm.Realm; +import io.realm.RealmResults; import timber.log.Timber; /** @@ -63,6 +76,8 @@ public class ERC721Token extends Token { private final Map tokenBalanceAssets; + private static final Map balanceChecks = new ConcurrentHashMap<>(); + private boolean batchProcessingError; public ERC721Token(TokenInfo tokenInfo, Map balanceList, BigDecimal balance, long blancaTime, String networkName, ContractType type) { @@ -77,6 +92,7 @@ public ERC721Token(TokenInfo tokenInfo, Map balanceList, B } setInterfaceSpec(type); group = TokenGroup.NFT; + batchProcessingError = false; } @Override @@ -259,7 +275,9 @@ public boolean checkRealmBalanceChange(RealmToken realmToken) public boolean checkBalanceChange(Token oldToken) { if (super.checkBalanceChange(oldToken)) return true; - if (getTokenAssets().size() != oldToken.getTokenAssets().size()) return true; + if ((getTokenAssets() != null && oldToken.getTokenAssets() != null) + && getTokenAssets().size() != oldToken.getTokenAssets().size()) return true; + for (BigInteger tokenId : tokenBalanceAssets.keySet()) { NFTAsset newAsset = tokenBalanceAssets.get(tokenId); @@ -326,6 +344,11 @@ private Event getTransferEvents() @Override public BigDecimal updateBalance(Realm realm) { + if (balanceChecks.containsKey(tokenInfo.address)) + { + return balance; + } + //first get current block SyncDef sync = eventSync.getSyncDef(realm); if (sync == null) return balance; @@ -339,11 +362,20 @@ public BigDecimal updateBalance(Realm realm) try { - final Web3j web3j = TokenRepository.getWeb3jService(tokenInfo.chainId); + balanceChecks.put(tokenInfo.address, true); //set checking + final Web3j web3j = TokenRepository.getWeb3jServiceForEvents(tokenInfo.chainId); + if (contractType == ContractType.ERC721_ENUMERABLE) + { + updateEnumerableBalance(web3j, realm); + } + Pair, HashSet>> evRead = eventSync.processTransferEvents(web3j, getTransferEvents(), startBlock, endBlock, realm); - eventSync.updateEventReads(realm, sync, currentBlock, evRead.first); //means our event read was fine + eventSync.updateEventReads(realm, sync, currentBlock, evRead.first); //means our event read was fine + + //No need to go any further if this is enumerable + if (contractType == ContractType.ERC721_ENUMERABLE) return balance; HashSet allMovingTokens = new HashSet<>(evRead.second.first); allMovingTokens.addAll(evRead.second.second); @@ -362,6 +394,7 @@ public BigDecimal updateBalance(Realm realm) if (eventSync.handleEthLogError(e.error, startBlock, endBlock, sync, realm)) { //recurse until we find a good value + balanceChecks.remove(tokenInfo.address); updateBalance(realm); } } @@ -369,10 +402,107 @@ public BigDecimal updateBalance(Realm realm) { Timber.w(e); } + finally + { + balanceChecks.remove(tokenInfo.address); + } + + //check for possible issues + if (endBlock == DefaultBlockParameterName.LATEST && balance.compareTo(BigDecimal.valueOf(tokenBalanceAssets.size())) != 0) + { + //possible mismatch, scan from beginning again + eventSync.resetEventReads(realm); + } return balance; } + /*********** + * For ERC721Enumerable interface + **********/ + private void updateEnumerableBalance(Web3j web3j, Realm realm) throws IOException + { + HashSet tokenIdsHeld = new HashSet<>(); + //get enumerable balance + //find tokenIds held + long currentBalance = balance != null ? balance.longValue() : 0; + + if (EthereumNetworkBase.getBatchProcessingLimit(tokenInfo.chainId) > 0 && !batchProcessingError && currentBalance > 1) //no need to do batch query for 1 + { + updateEnumerableBatchBalance(web3j, currentBalance, tokenIdsHeld, realm); + } + else + { + for (long tokenIndex = 0; tokenIndex < currentBalance; tokenIndex++) + { + // find tokenId from index + String tokenId = callSmartContractFunction(tokenInfo.chainId, tokenOfOwnerByIndex(BigInteger.valueOf(tokenIndex)), getAddress(), getWallet()); + if (tokenId == null) continue; + tokenIdsHeld.add(new BigInteger(tokenId)); + } + } + + updateRealmForEnumerable(realm, tokenIdsHeld); + } + + private void updateEnumerableBatchBalance(Web3j web3j, long currentBalance, HashSet tokenIdsHeld, Realm realm) throws IOException + { + BatchRequest requests = web3j.newBatch(); + + for (long tokenIndex = 0; tokenIndex < currentBalance; tokenIndex++) + { + requests.add(getContractCall(web3j, tokenOfOwnerByIndex(BigInteger.valueOf(tokenIndex)), getAddress())); + if (requests.getRequests().size() >= EthereumNetworkBase.getBatchProcessingLimit(tokenInfo.chainId)) + { + //do this send + handleEnumerableRequests(requests, tokenIdsHeld); + requests = web3j.newBatch(); + } + } + + if (requests.getRequests().size() > 0) + { + //do final call + handleEnumerableRequests(requests, tokenIdsHeld); + } + + if (batchProcessingError) + { + updateEnumerableBalance(web3j, realm); + } + } + + private void handleEnumerableRequests(BatchRequest requests, HashSet tokenIdsHeld) throws IOException + { + BatchResponse responses = requests.send(); + if (responses.getResponses().size() != requests.getRequests().size()) + { + batchProcessingError = true; + return; + } + + //process responses + for (Response rsp : responses.getResponses()) + { + BigInteger tokenId = getTokenId(rsp); + if (tokenId == null) continue; + tokenIdsHeld.add(tokenId); + } + } + + private BigInteger getTokenId(Response rsp) + { + List> outputParams = Utils.convert(Collections.singletonList(new TypeReference() {})); + List responseValues = FunctionReturnDecoder.decode(((EthCall)rsp).getValue(), outputParams); + if (!responseValues.isEmpty()) + { + String tokenIdStr = responseValues.get(0).getValue().toString(); + if (!TextUtils.isEmpty(tokenIdStr)) return new BigInteger(tokenIdStr); + } + + return null; + } + private void updateRealmBalance(Realm realm, Set tokenIds, Set allMovingTokens) { boolean updated = false; @@ -419,6 +549,40 @@ private void removeRealmBalance(Realm realm, HashSet removedTokens) }); } + private void updateRealmForEnumerable(Realm realm, HashSet currentTokens) + { + HashSet storedBalance = new HashSet<>(); + RealmResults results = realm.where(RealmNFTAsset.class) + .like("tokenIdAddr", databaseKey(this) + "-*", Case.INSENSITIVE) + .findAll(); + + for (RealmNFTAsset t : results) + { + storedBalance.add(new BigInteger(t.getTokenId())); + } + + if (!currentTokens.equals(storedBalance)) + { + realm.executeTransaction(r -> { + results.deleteAllFromRealm(); + for (BigInteger tokenId : currentTokens) + { + String key = RealmNFTAsset.databaseKey(this, tokenId); + RealmNFTAsset realmAsset = realm.where(RealmNFTAsset.class) + .equalTo("tokenIdAddr", key) + .findFirst(); + + if (realmAsset == null) + { + realmAsset = r.createObject(RealmNFTAsset.class, key); //create asset in realm + realmAsset.setMetaData(new NFTAsset(tokenId).jsonMetaData()); + r.insertOrUpdate(realmAsset); + } + } + }); + } + } + private void updateRealmBalances(Realm realm, Set tokenIds) { if (realm == null) return; @@ -463,13 +627,15 @@ public HashSet processLogsAndStoreTransferEvents(EthLog receiveLogs, return tokenIds; } - private HashSet checkBalances(Web3j web3j, HashSet eventIds) + private HashSet checkBalances(Web3j web3j, HashSet eventIds) throws IOException { HashSet heldTokens = new HashSet<>(); + if (EthereumNetworkBase.getBatchProcessingLimit(tokenInfo.chainId) > 0 && !batchProcessingError && eventIds.size() > 1) return checkBatchBalances(web3j, eventIds); + for (BigInteger tokenId : eventIds) { - String owner = callSmartContractFunction(web3j, ownerOf(tokenId), getAddress(), getWallet()); - if (owner == null || owner.toLowerCase().equals(getWallet())) + String owner = callSmartContractFunction(tokenInfo.chainId, ownerOf(tokenId), getAddress(), getWallet()); + if (owner == null || owner.equalsIgnoreCase(getWallet())) { heldTokens.add(tokenId); } @@ -478,6 +644,80 @@ private HashSet checkBalances(Web3j web3j, HashSet event return heldTokens; } + private HashSet checkBatchBalances(Web3j web3j, HashSet eventIds) throws IOException + { + HashSet heldTokens = new HashSet<>(); + List balanceIds = new ArrayList<>(); + BatchRequest requests = web3j.newBatch(); + for (BigInteger tokenId : eventIds) + { + requests.add(getContractCall(web3j, ownerOf(tokenId), getAddress())); + balanceIds.add(tokenId); + if (requests.getRequests().size() >= EthereumNetworkBase.getBatchProcessingLimit(tokenInfo.chainId)) + { + //do this send + handleRequests(requests, balanceIds, heldTokens); + requests = web3j.newBatch(); + } + } + + if (requests.getRequests().size() > 0) + { + //do final call + handleRequests(requests, balanceIds, heldTokens); + } + + if (batchProcessingError) + { + return checkBalances(web3j, eventIds); + } + else + { + return heldTokens; + } + } + + private void handleRequests(BatchRequest requests, List balanceIds, HashSet heldTokens) throws IOException + { + int index = 0; + BatchResponse responses = requests.send(); + if (responses.getResponses().size() != requests.getRequests().size()) + { + batchProcessingError = true; + return; + } + + //process responses + for (Response rsp : responses.getResponses()) + { + BigInteger tokenId = balanceIds.get(index); + if (isOwner(rsp, tokenId)) + { + heldTokens.add(tokenId); + } + + index++; + } + + balanceIds.clear(); + } + + private boolean isOwner(Response rsp, BigInteger tokenId) + { + EthCall response = (EthCall) rsp; + Function function = ownerOf(tokenId); + List responseValues = FunctionReturnDecoder.decode(response.getValue(), function.getOutputParameters()); + if (!responseValues.isEmpty()) + { + String owner = responseValues.get(0).getValue().toString(); + return (!owner.isEmpty() && owner.equalsIgnoreCase(getWallet())); + } + else + { + return false; + } + } + @Override public EthFilter getReceiveBalanceFilter(Event event, DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { @@ -510,24 +750,6 @@ public EthFilter getSendBalanceFilter(Event event, DefaultBlockParameter startBl return filter; } - /** - * Returns false if the Asset balance appears to be entries with only TokenId - indicating an ERC721Ticket - * - * @return - */ - @Override - public boolean checkBalanceType() - { - boolean onlyHasTokenId = true; - //if elements contain asset with only assetId then most likely this is a ticket. - for (NFTAsset a : tokenBalanceAssets.values()) - { - if (!a.isBlank()) onlyHasTokenId = false; - } - - return tokenBalanceAssets.size() == 0 || !onlyHasTokenId; - } - public String getTransferID(Transaction tx) { if (tx.transactionInput != null && tx.transactionInput.miscData.size() > 0) @@ -601,30 +823,32 @@ public BigDecimal getBalanceRaw() * If there is a token that previously was there, but now isn't, it could be because * the opensea call was split or that the owner transferred the token * - * @param assetMap Loaded Assets from Realm - * @return map of currently known live assets + * @param assetMap Loaded Assets which are new assets (don't add assets from opensea unless we double check here first) + * @return map of checked assets */ @Override public Map queryAssets(Map assetMap) { final Web3j web3j = TokenRepository.getWeb3jService(tokenInfo.chainId); - //check all tokens in this contract - assetMap.putAll(tokenBalanceAssets); + HashSet currentAssets = new HashSet<>(assetMap.keySet()); + + try + { + currentAssets = checkBalances(web3j, currentAssets); + } + catch (Exception e) + { + // + } - //now check balance for all tokenIds (note that ERC1155 has a batch balance check, ERC721 does not) for (Map.Entry entry : assetMap.entrySet()) { BigInteger checkId = entry.getKey(); NFTAsset checkAsset = entry.getValue(); //check balance - String owner = callSmartContractFunction(web3j, ownerOf(checkId), getAddress(), getWallet()); - if (owner == null) //play it safe. If there's no 'ownerOf' for an ERC721, it's something custom like ENS - { - checkAsset.setBalance(BigDecimal.ONE); - } - else if (owner.toLowerCase().equals(getWallet())) + if (currentAssets.contains(checkId)) { checkAsset.setBalance(BigDecimal.ONE); } @@ -633,61 +857,82 @@ else if (owner.toLowerCase().equals(getWallet())) checkAsset.setBalance(BigDecimal.ZERO); } - //add back into asset map - tokenBalanceAssets.put(checkId, checkAsset); + assetMap.put(checkId, checkAsset); } - return tokenBalanceAssets; + return assetMap; } - + + // Check for new/missing tokenBalanceAssets @Override - public List getChangeList(Map assetMap) + public Map getAssetChange(Map oldAssetList) { - //detect asset removal - List oldAssetIdList = new ArrayList<>(assetMap.keySet()); - oldAssetIdList.removeAll(tokenBalanceAssets.keySet()); + Map updatedAssets = new HashMap<>(); + // detect asset removal, first find new assets + HashSet changedAssetList = new HashSet<>(tokenBalanceAssets.keySet()); + changedAssetList.removeAll(oldAssetList.keySet()); + + HashSet unchangedAssets = new HashSet<>(tokenBalanceAssets.keySet()); + unchangedAssets.removeAll(changedAssetList); - List changeList = new ArrayList<>(oldAssetIdList); + // removed assets + HashSet removedAssets = new HashSet<>(oldAssetList.keySet()); + removedAssets.removeAll(tokenBalanceAssets.keySet()); + changedAssetList.addAll(removedAssets); + HashSet balanceAssets = new HashSet<>(); + + final Web3j web3j = TokenRepository.getWeb3jService(tokenInfo.chainId); + + try + { + balanceAssets = checkBalances(web3j, changedAssetList); + } + catch (Exception e) + { + // + } //Now detect differences or new tokens - for (BigInteger tokenId : tokenBalanceAssets.keySet()) + for (BigInteger tokenId : changedAssetList) { - NFTAsset newAsset = tokenBalanceAssets.get(tokenId); - NFTAsset oldAsset = assetMap.get(tokenId); + NFTAsset asset = tokenBalanceAssets.get(tokenId); + if (asset == null) asset = oldAssetList.get(tokenId); - if (oldAsset == null || newAsset.hashCode() != oldAsset.hashCode()) + if (asset == null) { - changeList.add(tokenId); + continue; } - } - return changeList; - } + if (balanceAssets.contains(tokenId)) + { + asset.setBalance(BigDecimal.ZERO); + } + else + { + asset.setBalance(BigDecimal.ONE); + } - private String callSmartContractFunction(Web3j web3j, - Function function, String contractAddress, String walletAddr) - { - String encodedFunction = FunctionEncoder.encode(function); + updatedAssets.put(tokenId, asset); + } - try + for (BigInteger tokenId : unchangedAssets) { - org.web3j.protocol.core.methods.request.Transaction transaction - = createEthCallTransaction(walletAddr, contractAddress, encodedFunction); - EthCall response = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).send(); - - List responseValues = FunctionReturnDecoder.decode(response.getValue(), function.getOutputParameters()); - - if (!responseValues.isEmpty()) + NFTAsset asset = tokenBalanceAssets.get(tokenId); + if (asset != null) { - return responseValues.get(0).getValue().toString(); + updatedAssets.put(tokenId, asset); } } - catch (Exception e) - { - // - } - return null; + return updatedAssets; + } + + private Request getContractCall(Web3j web3j, Function function, String contractAddress) + { + String encodedFunction = FunctionEncoder.encode(function); + org.web3j.protocol.core.methods.request.Transaction transaction + = createEthCallTransaction(getWallet(), contractAddress, encodedFunction); + return web3j.ethCall(transaction, DefaultBlockParameterName.LATEST); } private static Function ownerOf(BigInteger token) @@ -700,6 +945,13 @@ private static Function ownerOf(BigInteger token) })); } + private Function tokenOfOwnerByIndex(BigInteger index) + { + return new Function("tokenOfOwnerByIndex", + Arrays.asList(new Address(getWallet()), new Uint256(index)), + Collections.singletonList(new TypeReference() {})); + } + @Override public List getStandardFunctions() { diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Ticket.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Ticket.java index e909b34178..44e5b85115 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Ticket.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Ticket.java @@ -10,7 +10,6 @@ import com.alphawallet.app.entity.tokendata.TokenGroup; import com.alphawallet.app.repository.EventResult; import com.alphawallet.app.repository.entity.RealmToken; -import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.BaseViewModel; import com.alphawallet.token.entity.TicketRange; @@ -27,31 +26,34 @@ import java.util.List; /** - * Created by James on 27/01/2018. It might seem counter intuitive - * but here Ticket refers to a container of an asset class here, not - * the right to seat somewhere in the venue. Therefore, there - * shouldn't be List To understand this, imagine that one says - * "I have two cryptocurrencies: Ether and Bitcoin, each amounts to a - * hundred", and he pauses and said, "I also have two indices: FIFA - * and Formuler-one, which, too, amounts to a hundred each". + * Created by James on 27/01/2018. */ public class Ticket extends Token { private final List balanceArray; - private boolean isMatchedInXML = false; - public Ticket(TokenInfo tokenInfo, List balances, long blancaTime, String networkName, ContractType type) { + public Ticket(TokenInfo tokenInfo, List balances, long blancaTime, String networkName, ContractType type) + { super(tokenInfo, BigDecimal.ZERO, blancaTime, networkName, type); this.balanceArray = balances; - balance = balanceArray != null ? BigDecimal.valueOf(balanceArray.size()) : BigDecimal.ZERO; + balance = balanceArray != null ? BigDecimal.valueOf(getNonZeroArrayBalance().size()) : BigDecimal.ZERO; group = TokenGroup.NFT; } - public Ticket(TokenInfo tokenInfo, String balances, long blancaTime, String networkName, ContractType type) { + public Ticket(TokenInfo tokenInfo, String balances, long blancaTime, String networkName, ContractType type) + { super(tokenInfo, BigDecimal.ZERO, blancaTime, networkName, type); this.balanceArray = stringHexToBigIntegerList(balances); - balance = BigDecimal.valueOf(balanceArray.size()); + balance = BigDecimal.valueOf(getNonZeroArrayBalance().size()); + group = TokenGroup.NFT; + } + + public Ticket(Token oldTicket, List balances) + { + super(oldTicket.tokenInfo, BigDecimal.ZERO, oldTicket.updateBlancaTime, oldTicket.getNetworkName(), oldTicket.contractType); + this.balanceArray = balances; + balance = BigDecimal.valueOf(getNonZeroArrayBalance().size()); group = TokenGroup.NFT; } @@ -235,11 +237,6 @@ private List tokenIdsToTokenIndices(List tokenIds) return indexList; } - public void checkIsMatchedInXML(AssetDefinitionService assetService) - { - isMatchedInXML = assetService.hasDefinition(tokenInfo.chainId, tokenInfo.address); - } - @Override public Function getTransferFunction(String to, List tokenIndices) throws NumberFormatException { @@ -305,7 +302,10 @@ public boolean hasArrayBalance() public List getNonZeroArrayBalance() { List nonZeroValues = new ArrayList<>(); - for (BigInteger value : balanceArray) if (value.compareTo(BigInteger.ZERO) != 0 && !nonZeroValues.contains(value)) nonZeroValues.add(value); + for (BigInteger value : balanceArray) + { + if (value.compareTo(BigInteger.ZERO) != 0) nonZeroValues.add(value); + } return nonZeroValues; } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java index ba8c2e4acd..0108c12332 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java @@ -49,6 +49,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -202,6 +203,18 @@ public String getFullName(AssetDefinitionService assetDefinition, int count) } } + public String getTokenSymbol(Token token){ + + if (!TextUtils.isEmpty(token.tokenInfo.symbol) && token.tokenInfo.symbol.length() > 1) + { + return Utils.getIconisedText(token.tokenInfo.symbol); + } + else + { + return Utils.getIconisedText(token.getName()); + } + } + public String getTSName(AssetDefinitionService assetDefinition, int count) { String name = assetDefinition != null ? assetDefinition.getTokenName(tokenInfo.chainId, tokenInfo.address, count) : null; if (name != null) { @@ -370,12 +383,12 @@ public void setIsEthereum() public boolean isBad() { - return tokenInfo == null || (tokenInfo.symbol == null && tokenInfo.name == null); + return tokenInfo == null || tokenInfo.chainId == 0 || (tokenInfo.symbol == null && tokenInfo.name == null); } public void setTokenWallet(String address) { - this.tokenWallet = address; + this.tokenWallet = address.toLowerCase(Locale.ROOT); } public void setupRealmToken(RealmToken realmToken) @@ -698,11 +711,6 @@ public int hashCode() return hash; } - public boolean checkBalanceType() - { - return true; - } - public String getTransactionDetail(Context ctx, Transaction tx, TokensService tService) { if (isEthereum()) @@ -835,6 +843,7 @@ public boolean needsTransactionCheck() case OTHER: case NOT_SET: case ERC721: + case ERC721_ENUMERABLE: case ERC721_LEGACY: case ERC721_UNDETERMINED: case CREATION: @@ -908,11 +917,6 @@ public boolean mayRequireRefresh() || (!TextUtils.isEmpty(tokenInfo.symbol) && tokenInfo.symbol.contains("?")); } - public List getChangeList(Map assetMap) - { - return new ArrayList<>(); - } - public void setAssetContract(AssetContract contract) { } public AssetContract getAssetContract() { return null; } @@ -931,6 +935,11 @@ public Map queryAssets(Map assetMap) return assetMap; } + public Map getAssetChange(Map oldAssetList) + { + return oldAssetList; + } + /** * Token Metadata handling */ @@ -1005,4 +1014,4 @@ public HashSet processLogsAndStoreTransferEvents(EthLog receiveLogs, { return null; } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java index da4baba593..099aeaa42a 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java @@ -8,7 +8,6 @@ import android.os.Parcelable; import android.text.TextUtils; import android.text.format.DateUtils; -import android.util.Pair; import androidx.annotation.NonNull; @@ -18,11 +17,6 @@ import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.service.AssetDefinitionService; -import com.alphawallet.app.service.TokensService; -import com.alphawallet.app.ui.widget.holder.TokenHolder; - -import java.math.BigDecimal; -import java.math.RoundingMode; /** * Created by JB on 12/07/2020. @@ -261,10 +255,11 @@ public float calculateBalanceUpdateWeight() { float updateWeight = 0; //calculate balance update time + long timeDiff = (System.currentTimeMillis() - lastUpdate) / DateUtils.SECOND_IN_MILLIS; + if (isEthereum()) { - long currentTime = System.currentTimeMillis(); - if (lastUpdate < currentTime - 30 * DateUtils.SECOND_IN_MILLIS) + if (timeDiff > 30) { updateWeight = 2.0f; } @@ -278,15 +273,15 @@ else if (hasValidName()) if (isNFT()) { //ERC721 types which usually get their balance from opensea. Still need to check the balance for stale tokens to spot a positive -> zero balance transition - updateWeight = 0.25f; + updateWeight = (timeDiff > 30) ? 0.25f : 0.01f; } else if (isEnabled) { - updateWeight = hasPositiveBalance() ? 1.0f : 0.5f; //30 seconds + updateWeight = hasPositiveBalance() ? 1.0f : 0.1f; //30 seconds } else { - updateWeight = 0.25f; //1 minute + updateWeight = 0.1f; //1 minute } } return updateWeight; @@ -307,4 +302,12 @@ public boolean equals(TokenCardMeta other) { return (tokenId.equalsIgnoreCase(other.tokenId)); } + + //30 seconds for enabled with balance, 120 seconds for enabled zero balance, 300 for not enabled + //Note that if we pick up a balance change for non-enabled token that hasn't been locked out, it'll appear with the transfer sweep + public long calculateUpdateFrequency() + { + long timeInSeconds = isEnabled ? (hasPositiveBalance() ? 30 : 120) : 300; + return timeInSeconds * DateUtils.SECOND_IN_MILLIS; + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java index b367ac04f5..c5b8843e8f 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java @@ -34,6 +34,7 @@ public Token createToken(TokenInfo tokenInfo, BigDecimal balance, List STL Office Token" + + " STL Office Tokens Boleto de admisión" + + " Boleto de admisiónes 入場券" + + " 入場券 " + + " 0xC3eeCa3Feb9Dbc06c6e749702AcB8d56A07BFb05 " + + " Unlock 开锁" + + " Abrir " + + " " + + " Lock 关锁" + + " Cerrar " + + " Mint Ape 1 " + + " " + + " Mint Ape 2 " + + " " + + " Mint STL Token" + + " " + + " " + + " "; +} diff --git a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java index 0dac3b5df9..a7a9689839 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java @@ -125,6 +125,10 @@ public String calcMD5() String rand = String.valueOf(new Random(System.currentTimeMillis()).nextInt()); sb.append(rand); //never matches } + catch (Exception e) + { + Timber.w(e); + } //return complete hash return sb.toString(); diff --git a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java index 6c2f43a6fc..9d1d42e18f 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java @@ -1,8 +1,10 @@ package com.alphawallet.app.entity.tokenscript; +import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; +import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; + import android.text.TextUtils; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.util.BalanceUtils; @@ -11,12 +13,12 @@ import com.alphawallet.token.entity.Attribute; import com.alphawallet.token.entity.AttributeInterface; import com.alphawallet.token.entity.ContractAddress; -import com.alphawallet.token.entity.EventDefinition; import com.alphawallet.token.entity.FunctionDefinition; import com.alphawallet.token.entity.MethodArg; import com.alphawallet.token.entity.TokenScriptResult; import com.alphawallet.token.entity.TokenscriptElement; import com.alphawallet.token.entity.TransactionResult; +import com.alphawallet.token.entity.ViewType; import com.alphawallet.token.tools.TokenDefinition; import org.web3j.abi.FunctionEncoder; @@ -29,7 +31,102 @@ import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Uint; import org.web3j.abi.datatypes.Utf8String; -import org.web3j.abi.datatypes.generated.*; +import org.web3j.abi.datatypes.generated.Bytes1; +import org.web3j.abi.datatypes.generated.Bytes10; +import org.web3j.abi.datatypes.generated.Bytes11; +import org.web3j.abi.datatypes.generated.Bytes12; +import org.web3j.abi.datatypes.generated.Bytes13; +import org.web3j.abi.datatypes.generated.Bytes14; +import org.web3j.abi.datatypes.generated.Bytes15; +import org.web3j.abi.datatypes.generated.Bytes16; +import org.web3j.abi.datatypes.generated.Bytes17; +import org.web3j.abi.datatypes.generated.Bytes18; +import org.web3j.abi.datatypes.generated.Bytes19; +import org.web3j.abi.datatypes.generated.Bytes2; +import org.web3j.abi.datatypes.generated.Bytes20; +import org.web3j.abi.datatypes.generated.Bytes21; +import org.web3j.abi.datatypes.generated.Bytes22; +import org.web3j.abi.datatypes.generated.Bytes23; +import org.web3j.abi.datatypes.generated.Bytes24; +import org.web3j.abi.datatypes.generated.Bytes25; +import org.web3j.abi.datatypes.generated.Bytes26; +import org.web3j.abi.datatypes.generated.Bytes27; +import org.web3j.abi.datatypes.generated.Bytes28; +import org.web3j.abi.datatypes.generated.Bytes29; +import org.web3j.abi.datatypes.generated.Bytes3; +import org.web3j.abi.datatypes.generated.Bytes30; +import org.web3j.abi.datatypes.generated.Bytes31; +import org.web3j.abi.datatypes.generated.Bytes32; +import org.web3j.abi.datatypes.generated.Bytes4; +import org.web3j.abi.datatypes.generated.Bytes5; +import org.web3j.abi.datatypes.generated.Bytes6; +import org.web3j.abi.datatypes.generated.Bytes7; +import org.web3j.abi.datatypes.generated.Bytes8; +import org.web3j.abi.datatypes.generated.Bytes9; +import org.web3j.abi.datatypes.generated.Int104; +import org.web3j.abi.datatypes.generated.Int112; +import org.web3j.abi.datatypes.generated.Int120; +import org.web3j.abi.datatypes.generated.Int128; +import org.web3j.abi.datatypes.generated.Int136; +import org.web3j.abi.datatypes.generated.Int144; +import org.web3j.abi.datatypes.generated.Int152; +import org.web3j.abi.datatypes.generated.Int16; +import org.web3j.abi.datatypes.generated.Int160; +import org.web3j.abi.datatypes.generated.Int168; +import org.web3j.abi.datatypes.generated.Int176; +import org.web3j.abi.datatypes.generated.Int184; +import org.web3j.abi.datatypes.generated.Int192; +import org.web3j.abi.datatypes.generated.Int200; +import org.web3j.abi.datatypes.generated.Int208; +import org.web3j.abi.datatypes.generated.Int216; +import org.web3j.abi.datatypes.generated.Int224; +import org.web3j.abi.datatypes.generated.Int232; +import org.web3j.abi.datatypes.generated.Int24; +import org.web3j.abi.datatypes.generated.Int240; +import org.web3j.abi.datatypes.generated.Int248; +import org.web3j.abi.datatypes.generated.Int256; +import org.web3j.abi.datatypes.generated.Int32; +import org.web3j.abi.datatypes.generated.Int40; +import org.web3j.abi.datatypes.generated.Int48; +import org.web3j.abi.datatypes.generated.Int56; +import org.web3j.abi.datatypes.generated.Int64; +import org.web3j.abi.datatypes.generated.Int72; +import org.web3j.abi.datatypes.generated.Int8; +import org.web3j.abi.datatypes.generated.Int80; +import org.web3j.abi.datatypes.generated.Int88; +import org.web3j.abi.datatypes.generated.Int96; +import org.web3j.abi.datatypes.generated.Uint104; +import org.web3j.abi.datatypes.generated.Uint112; +import org.web3j.abi.datatypes.generated.Uint120; +import org.web3j.abi.datatypes.generated.Uint128; +import org.web3j.abi.datatypes.generated.Uint136; +import org.web3j.abi.datatypes.generated.Uint144; +import org.web3j.abi.datatypes.generated.Uint152; +import org.web3j.abi.datatypes.generated.Uint16; +import org.web3j.abi.datatypes.generated.Uint160; +import org.web3j.abi.datatypes.generated.Uint168; +import org.web3j.abi.datatypes.generated.Uint176; +import org.web3j.abi.datatypes.generated.Uint184; +import org.web3j.abi.datatypes.generated.Uint192; +import org.web3j.abi.datatypes.generated.Uint200; +import org.web3j.abi.datatypes.generated.Uint208; +import org.web3j.abi.datatypes.generated.Uint216; +import org.web3j.abi.datatypes.generated.Uint224; +import org.web3j.abi.datatypes.generated.Uint232; +import org.web3j.abi.datatypes.generated.Uint24; +import org.web3j.abi.datatypes.generated.Uint240; +import org.web3j.abi.datatypes.generated.Uint248; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.abi.datatypes.generated.Uint32; +import org.web3j.abi.datatypes.generated.Uint40; +import org.web3j.abi.datatypes.generated.Uint48; +import org.web3j.abi.datatypes.generated.Uint56; +import org.web3j.abi.datatypes.generated.Uint64; +import org.web3j.abi.datatypes.generated.Uint72; +import org.web3j.abi.datatypes.generated.Uint8; +import org.web3j.abi.datatypes.generated.Uint80; +import org.web3j.abi.datatypes.generated.Uint88; +import org.web3j.abi.datatypes.generated.Uint96; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.request.EthFilter; @@ -50,12 +147,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import io.reactivex.Observable; +import io.reactivex.Single; import timber.log.Timber; -import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; -import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; - /** * Created by James on 13/06/2019. * Stormbird in Sydney @@ -73,14 +167,14 @@ public Function generateTransactionFunction(Token token, BigInteger tokenId, Tok //pre-parse tokenId. if (tokenId.bitCount() > 256) tokenId = tokenId.or(BigInteger.ONE.shiftLeft(256).subtract(BigInteger.ONE)); //truncate tokenId too large - List params = new ArrayList(); - List> returnTypes = new ArrayList>(); + List params = new ArrayList<>(); + List> returnTypes = new ArrayList<>(); for (MethodArg arg : function.parameters) { String value = resolveReference(token, arg.element, tokenId, definition, attrIf); //get arg.element.value in the form of BigInteger if appropriate - byte[] argValueBytes = null; - BigInteger argValueBI = null; + byte[] argValueBytes = {0}; + BigInteger argValueBI = BigInteger.ZERO; if (valueNotFound) { @@ -432,7 +526,7 @@ public Function generateTransactionFunction(Token token, BigInteger tokenId, Tok } break; default: - Timber.d("NOT IMPLEMENTED: " + arg.parameterType); + Timber.d("NOT IMPLEMENTED: %s", arg.parameterType); break; } } @@ -528,7 +622,7 @@ private String handleTransactionResult(TransactionResult result, Function functi { case Boolean: value = Numeric.toBigInt(hexBytes); - transResult = value.equals(BigDecimal.ZERO) ? "FALSE" : "TRUE"; + transResult = value.equals(BigInteger.ZERO) ? "FALSE" : "TRUE"; break; case Integer: value = Numeric.toBigInt(hexBytes); @@ -590,7 +684,7 @@ else if (val.getTypeAsString().equals("address")) return transResult; } - private String checkBytesString(String responseValue) throws Exception + private String checkBytesString(String responseValue) { String name = ""; if (responseValue.length() > 0) @@ -618,7 +712,7 @@ private String checkBytesString(String responseValue) throws Exception public TokenScriptResult.Attribute parseFunctionResult(TransactionResult transactionResult, Attribute attr) { String res = attr.getSyntaxVal(transactionResult.result); - BigInteger val = transactionResult.tokenId; //? + BigInteger val = transactionResult.tokenId; if (attr.syntax == TokenDefinition.Syntax.Boolean) { @@ -644,7 +738,7 @@ else if (transactionResult.result.startsWith("0x")) val = BigInteger.ZERO; } } - return new TokenScriptResult.Attribute(attr.name, attr.label, val, res); + return new TokenScriptResult.Attribute(attr, val, res); } public static final String ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; @@ -656,10 +750,10 @@ else if (transactionResult.result.startsWith("0x")) * @param definition * @return */ - public Observable fetchResultFromEthereum(Token token, ContractAddress contractAddress, Attribute attr, + public Single fetchResultFromEthereum(Token token, ContractAddress contractAddress, Attribute attr, BigInteger tokenId, TokenDefinition definition, AttributeInterface attrIf) { - return Observable.fromCallable(() -> { + return Single.fromCallable(() -> { TransactionResult transactionResult = new TransactionResult(contractAddress.chainId, contractAddress.address, tokenId, attr); Function transaction = generateTransactionFunction(token, tokenId, definition, attr.function, attrIf); @@ -672,7 +766,7 @@ public Observable fetchResultFromEthereum(Token token, Contra else { //now push the transaction - result = callSmartContractFunction(TokenRepository.getWeb3jService(contractAddress.chainId), transaction, contractAddress.address, ZERO_ADDRESS); + result = callSmartContractFunction(TokenRepository.getWeb3jService(contractAddress.chainId), transaction, contractAddress.address, token.getWallet()); } transactionResult.result = handleTransactionResult(transactionResult, transaction, result, attr, System.currentTimeMillis()); @@ -753,7 +847,7 @@ else if (!TextUtils.isEmpty(element.value)) } else { - return fetchAttrResult(token, attr, tokenId, definition, attrIf, false).blockingSingle().text; + return fetchAttrResult(token, attr, tokenId, definition, attrIf, ViewType.VIEW).blockingGet().text; } return null; @@ -781,28 +875,29 @@ else if (!TextUtils.isEmpty(element.value)) * @return */ - public Observable fetchAttrResult(Token token, Attribute attr, BigInteger tokenId, - TokenDefinition td, AttributeInterface attrIf, boolean itemView) + public Single fetchAttrResult(Token token, Attribute attr, BigInteger tokenId, + TokenDefinition td, AttributeInterface attrIf, ViewType itemView) { + final BigInteger useTokenId = (attr == null || !attr.usesTokenId()) ? BigInteger.ZERO : tokenId; if (attr == null) { - return Observable.fromCallable(() -> new TokenScriptResult.Attribute("bd", "bd", BigInteger.ZERO, "")); + return Single.fromCallable(() -> new TokenScriptResult.Attribute("bd", "bd", BigInteger.ZERO, "")); } - else if (token.getAttributeResult(attr.name, tokenId) != null) + else if (token.getAttributeResult(attr.name, useTokenId) != null) { - return Observable.fromCallable(() -> token.getAttributeResult(attr.name, tokenId)); + return Single.fromCallable(() -> token.getAttributeResult(attr.name, useTokenId)); } else if (attr.event != null) { //retrieve events from DB ContractAddress useAddress = new ContractAddress(attr.event.contract.addresses.keySet().iterator().next(), attr.event.contract.addresses.values().iterator().next().get(0)); - TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, tokenId); //Needs to allow for multiple tokenIds + TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, useTokenId); //Needs to allow for multiple tokenIds //if the latest event result is not yet found, then find it here if (TextUtils.isEmpty(cachedResult.result)) { //try to fetch latest event result - this can happen at startup - return getEventResult(cachedResult, attr, tokenId, attrIf); + return getEventResult(cachedResult, attr, useTokenId, attrIf); } else { @@ -811,22 +906,22 @@ else if (attr.event != null) } else if (attr.function == null) // static attribute from tokenId (eg city mapping from tokenId) { - return staticAttribute(attr, tokenId); + return staticAttribute(attr, useTokenId); } else { ContractAddress useAddress = new ContractAddress(attr.function); //always use the function attribute's address long lastTxUpdate = attrIf.getLastTokenUpdate(useAddress.chainId, useAddress.address); - TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, tokenId); //Needs to allow for multiple tokenIds - if ((itemView || (!attr.isVolatile() && ((attrIf.resolveOptimisedAttr(useAddress, attr, cachedResult) || !cachedResult.needsUpdating(lastTxUpdate)))))) //can we use wallet's known data or cached value? + TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, useTokenId); //Needs to allow for multiple tokenIds + if ((itemView == ViewType.ITEM_VIEW || (!attr.isVolatile() && ((attrIf.resolveOptimisedAttr(useAddress, attr, cachedResult) || !cachedResult.needsUpdating(lastTxUpdate)))))) //can we use wallet's known data or cached value? { return resultFromDatabase(cachedResult, attr); } else //if cached value is invalid or if value is dynamic { final String walletAddress = attrIf.getWalletAddr(); - return fetchResultFromEthereum(token, useAddress, attr, tokenId, td, attrIf) // Fetch function result from blockchain - .map(transactionResult -> addParseResultIfValid(token, tokenId, attr, transactionResult))// only cache live transaction result + return fetchResultFromEthereum(token, useAddress, attr, useTokenId, td, attrIf) // Fetch function result from blockchain + .map(transactionResult -> addParseResultIfValid(token, useTokenId, attr, transactionResult))// only cache live transaction result .map(result -> restoreFromDBIfRequired(result, cachedResult)) // If network unavailable restore value from cache .map(txResult -> attrIf.storeAuxData(walletAddress, txResult)) // store new data .map(result -> parseFunctionResult(result, attr)); // write returned data into attribute @@ -834,10 +929,10 @@ else if (attr.function == null) // static attribute from tokenId (eg city mappi } } - private Observable getEventResult(TransactionResult txResult, Attribute attr, BigInteger tokenId, AttributeInterface attrIf) + private Single getEventResult(TransactionResult txResult, Attribute attr, BigInteger tokenId, AttributeInterface attrIf) { //fetch the function - return Observable.fromCallable(() -> { + return Single.fromCallable(() -> { String walletAddress = attrIf.getWalletAddr(); Web3j web3j = getWeb3jService(attr.event.getEventChainId()); List tokenIds = new ArrayList<>(Collections.singletonList(tokenId)); @@ -846,7 +941,7 @@ private Observable getEventResult(TransactionResult //use last received log if (ethLogs.getLogs().size() > 0) { - EthLog.LogResult ethLog = ethLogs.getLogs().get(ethLogs.getLogs().size() - 1); + EthLog.LogResult ethLog = ethLogs.getLogs().get(ethLogs.getLogs().size() - 1); String selectVal = EventUtils.getSelectVal(attr.event, ethLog); txResult.result = attr.getSyntaxVal(selectVal); txResult.resultTime = ((Log)ethLog.get()).getBlockNumber().longValue(); @@ -857,9 +952,9 @@ private Observable getEventResult(TransactionResult }); } - private Observable staticAttribute(Attribute attr, BigInteger tokenId) + private Single staticAttribute(Attribute attr, BigInteger tokenId) { - return Observable.fromCallable(() -> { + return Single.fromCallable(() -> { try { if (attr.userInput) @@ -870,19 +965,19 @@ private Observable staticAttribute(Attribute attr, { BigInteger val = tokenId.and(attr.bitmask).shiftRight(attr.bitshift); Timber.d("ATTR: " + attr.label + " : " + attr.name + " : " + attr.getSyntaxVal(attr.toString(val))); - return new TokenScriptResult.Attribute(attr.name, attr.label, val, attr.getSyntaxVal(attr.toString(val))); + return new TokenScriptResult.Attribute(attr, val, attr.getSyntaxVal(attr.toString(val))); } } catch (Exception e) { - return new TokenScriptResult.Attribute(attr.name, attr.label, tokenId, "unsupported encoding"); + return new TokenScriptResult.Attribute(attr, tokenId, "unsupported encoding"); } }); } - private Observable resultFromDatabase(TransactionResult transactionResult, Attribute attr) + private Single resultFromDatabase(TransactionResult transactionResult, Attribute attr) { - return Observable.fromCallable(() -> parseFunctionResult(transactionResult, attr)); + return Single.fromCallable(() -> parseFunctionResult(transactionResult, attr)); } /** diff --git a/app/src/main/java/com/alphawallet/app/entity/unstoppable/GetRecordsResult.java b/app/src/main/java/com/alphawallet/app/entity/unstoppable/GetRecordsResult.java new file mode 100644 index 0000000000..ebc5ccb485 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/unstoppable/GetRecordsResult.java @@ -0,0 +1,17 @@ +package com.alphawallet.app.entity.unstoppable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; + +public class GetRecordsResult +{ + @SerializedName("meta") + @Expose + public Meta meta; + + @SerializedName("records") + @Expose + public HashMap records; +} diff --git a/app/src/main/java/com/alphawallet/app/entity/unstoppable/Meta.java b/app/src/main/java/com/alphawallet/app/entity/unstoppable/Meta.java new file mode 100644 index 0000000000..29104c96ea --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/unstoppable/Meta.java @@ -0,0 +1,35 @@ +package com.alphawallet.app.entity.unstoppable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Meta +{ + @SerializedName("resolver") + @Expose + public String resolver; + + @SerializedName("blockchain") + @Expose + public String blockchain; + + @SerializedName("networkId") + @Expose + public long networkId; + + @SerializedName("registry") + @Expose + public String registry; + + @SerializedName("domain") + @Expose + public String domain; + + @SerializedName("owner") + @Expose + public String owner; + + @SerializedName("reverse") + @Expose + public boolean reverse; +} diff --git a/app/src/main/java/com/alphawallet/app/entity/walletconnect/NamespaceParser.java b/app/src/main/java/com/alphawallet/app/entity/walletconnect/NamespaceParser.java new file mode 100644 index 0000000000..f80c6f2b4a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/walletconnect/NamespaceParser.java @@ -0,0 +1,72 @@ +package com.alphawallet.app.entity.walletconnect; + +import com.walletconnect.sign.client.Sign; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class NamespaceParser +{ + private final List chains = new ArrayList<>(); + private final List methods = new ArrayList<>(); + private final List events = new ArrayList<>(); + private final List wallets = new ArrayList<>(); + + public void parseProposal(Map requiredNamespaces) + { + for (Map.Entry entry : requiredNamespaces.entrySet()) + { + chains.addAll(entry.getValue().getChains()); + methods.addAll(entry.getValue().getMethods()); + events.addAll(entry.getValue().getEvents()); + } + } + + public void parseSession(Map namespaces) + { + for (Map.Entry entry : namespaces.entrySet()) + { + chains.addAll(parseChains(entry.getValue().getAccounts())); + methods.addAll(entry.getValue().getMethods()); + events.addAll(entry.getValue().getEvents()); + wallets.addAll(parseWallets(entry.getValue().getAccounts())); + } + } + + private List parseWallets(List accounts) + { + return accounts.stream() + .map((account) -> account.substring(account.lastIndexOf(":") + 1)) + .collect(Collectors.toList()); + } + + private List parseChains(List accounts) + { + return accounts.stream() + .map((account) -> account.substring(0, account.lastIndexOf(":"))) + .collect(Collectors.toList()); + } + + public List getChains() + { + return chains; + } + + public List getMethods() + { + return methods; + } + + public List getEvents() + { + return events; + } + + public List getWallets() + { + return new ArrayList<>(new HashSet<>(wallets)); + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectSessionItem.java b/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectSessionItem.java index b5df80e809..8e0e48c195 100644 --- a/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectSessionItem.java +++ b/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectSessionItem.java @@ -7,20 +7,28 @@ */ public class WalletConnectSessionItem { - public final String name; - public final String url; - public final String icon; - public final String sessionId; - public final String localSessionId; - public final long chainId; + public String name = ""; + public String url = ""; + public String icon = ""; + public String sessionId; + public String localSessionId; + public long chainId; public WalletConnectSessionItem(RealmWCSession s) { - name = s.getRemotePeerData().getName(); - url = s.getRemotePeerData().getUrl(); - icon = s.getRemotePeerData().getIcons().size() > 0 ? s.getRemotePeerData().getIcons().get(0) : null; + if (s.getRemotePeerData() != null) + { + name = s.getRemotePeerData().getName(); + url = s.getRemotePeerData().getUrl(); + icon = s.getRemotePeerData().getIcons().size() > 0 ? s.getRemotePeerData().getIcons().get(0) : null; + } sessionId = s.getSession().getTopic(); localSessionId = s.getSessionId(); chainId = s.getChainId() == 0 ? 1 : s.getChainId(); //older sessions without chainId set must be mainnet } -} \ No newline at end of file + + public WalletConnectSessionItem() + { + + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectV2SessionItem.java b/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectV2SessionItem.java new file mode 100644 index 0000000000..f02562247a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/walletconnect/WalletConnectV2SessionItem.java @@ -0,0 +1,100 @@ +package com.alphawallet.app.entity.walletconnect; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.walletconnect.sign.client.Sign; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class WalletConnectV2SessionItem extends WalletConnectSessionItem implements Parcelable +{ + public boolean settled; + public List chains = new ArrayList<>(); + public List wallets = new ArrayList<>(); + public List methods = new ArrayList<>(); + + public WalletConnectV2SessionItem(Sign.Model.Session s) + { + super(); + name = Objects.requireNonNull(s.getMetaData()).getName(); + url = Objects.requireNonNull(s.getMetaData()).getUrl(); + icon = s.getMetaData().getIcons().isEmpty() ? null : s.getMetaData().getIcons().get(0); + sessionId = s.getTopic(); + localSessionId = s.getTopic(); + settled = true; + NamespaceParser namespaceParser = new NamespaceParser(); + namespaceParser.parseSession(s.getNamespaces()); + chains = namespaceParser.getChains(); + methods = namespaceParser.getMethods(); + wallets = namespaceParser.getWallets(); + } + + public WalletConnectV2SessionItem(Parcel in) + { + name = in.readString(); + url = in.readString(); + icon = in.readString(); + sessionId = in.readString(); + localSessionId = in.readString(); + settled = in.readInt() == 1; + in.readStringList(chains); + in.readStringList(wallets); + in.readStringList(methods); + } + + public WalletConnectV2SessionItem() + { + } + + public static WalletConnectV2SessionItem from(Sign.Model.SessionProposal sessionProposal) + { + WalletConnectV2SessionItem item = new WalletConnectV2SessionItem(); + item.name = sessionProposal.getName(); + item.url = sessionProposal.getUrl(); + item.icon = sessionProposal.getIcons().isEmpty() ? null : sessionProposal.getIcons().get(0).toString(); + item.sessionId = sessionProposal.getProposerPublicKey(); + item.settled = false; + NamespaceParser namespaceParser = new NamespaceParser(); + namespaceParser.parseProposal(sessionProposal.getRequiredNamespaces()); + item.chains.addAll(namespaceParser.getChains()); + item.methods.addAll(namespaceParser.getMethods()); + return item; + } + + @Override + public int describeContents() + { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) + { + dest.writeString(name); + dest.writeString(url); + dest.writeString(icon); + dest.writeString(sessionId); + dest.writeString(localSessionId); + dest.writeInt(settled ? 1 : 0); + dest.writeStringList(chains); + dest.writeStringList(wallets); + dest.writeStringList(methods); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() + { + public WalletConnectV2SessionItem createFromParcel(Parcel in) + { + return new WalletConnectV2SessionItem(in); + } + + @Override + public WalletConnectV2SessionItem[] newArray(int size) + { + return new WalletConnectV2SessionItem[0]; + } + }; +} diff --git a/app/src/main/java/com/alphawallet/app/interact/CreateTransactionInteract.java b/app/src/main/java/com/alphawallet/app/interact/CreateTransactionInteract.java index 99d94e91d8..c24bd876f7 100644 --- a/app/src/main/java/com/alphawallet/app/interact/CreateTransactionInteract.java +++ b/app/src/main/java/com/alphawallet/app/interact/CreateTransactionInteract.java @@ -28,15 +28,15 @@ public CreateTransactionInteract(TransactionRepositoryType transactionRepository this.transactionRepository = transactionRepository; } - public Single sign(Wallet wallet, MessagePair messagePair, long chainId) + public Single sign(Wallet wallet, MessagePair messagePair) { - return transactionRepository.getSignature(wallet, messagePair, chainId) + return transactionRepository.getSignature(wallet, messagePair) .map(sig -> new SignaturePair(messagePair.selection, sig.signature, messagePair.message)); } - public Single sign(Wallet wallet, Signable message, long chainId) + public Single sign(Wallet wallet, Signable message) { - return transactionRepository.getSignature(wallet, message, chainId); + return transactionRepository.getSignature(wallet, message); } public Single create(Wallet from, String to, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, byte[] data, long chainId) @@ -117,4 +117,4 @@ public Single signTransaction(Wallet from, Web3Transaction web3 web3Tx.gasPrice, web3Tx.gasLimit, web3Tx.nonce, !TextUtils.isEmpty(web3Tx.payload) ? Numeric.hexStringToByteArray(web3Tx.payload) : new byte[0], chainId); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/interact/GenericWalletInteract.java b/app/src/main/java/com/alphawallet/app/interact/GenericWalletInteract.java index 2be594ab6f..4e40c949bc 100644 --- a/app/src/main/java/com/alphawallet/app/interact/GenericWalletInteract.java +++ b/app/src/main/java/com/alphawallet/app/interact/GenericWalletInteract.java @@ -3,9 +3,6 @@ import static com.alphawallet.app.C.ETHER_DECIMALS; import static com.alphawallet.app.entity.tokens.Token.TOKEN_BALANCE_PRECISION; -import android.util.Log; - -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.repository.WalletItem; import com.alphawallet.app.repository.WalletRepositoryType; @@ -33,6 +30,13 @@ public Single find() { .observeOn(AndroidSchedulers.mainThread()); } + public Single findWallet(String account) + { + return walletRepository.findWallet(account) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + /** * Called when wallet marked as backed up. * Update the wallet date diff --git a/app/src/main/java/com/alphawallet/app/interact/ImportWalletInteract.java b/app/src/main/java/com/alphawallet/app/interact/ImportWalletInteract.java index da4cfc7d58..4fb65a751a 100644 --- a/app/src/main/java/com/alphawallet/app/interact/ImportWalletInteract.java +++ b/app/src/main/java/com/alphawallet/app/interact/ImportWalletInteract.java @@ -1,7 +1,7 @@ package com.alphawallet.app.interact; import com.alphawallet.app.repository.WalletRepositoryType; -import com.alphawallet.app.util.AWEnsResolver; +import com.alphawallet.app.util.ens.AWEnsResolver; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; diff --git a/app/src/main/java/com/alphawallet/app/interact/WalletConnectInteract.java b/app/src/main/java/com/alphawallet/app/interact/WalletConnectInteract.java new file mode 100644 index 0000000000..01fc497ac1 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/interact/WalletConnectInteract.java @@ -0,0 +1,131 @@ +package com.alphawallet.app.interact; + +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.IBinder; + +import com.alphawallet.app.entity.WalletConnectActions; +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.entity.walletconnect.WalletConnectV2SessionItem; +import com.alphawallet.app.repository.entity.RealmWCSession; +import com.alphawallet.app.service.RealmManager; +import com.alphawallet.app.service.WalletConnectService; +import com.alphawallet.app.viewmodel.WalletConnectViewModel; +import com.alphawallet.app.walletconnect.WCClient; +import com.alphawallet.app.walletconnect.entity.WCUtils; +import com.walletconnect.sign.client.Sign; +import com.walletconnect.sign.client.SignClient; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import io.realm.Realm; +import io.realm.RealmResults; +import io.realm.Sort; +import timber.log.Timber; + +public class WalletConnectInteract +{ + private final RealmManager realmManager; + + @Inject + public WalletConnectInteract(RealmManager realmManager) + { + this.realmManager = realmManager; + } + + public int getSessionsCount() + { + return getSessions().size(); + } + + public List getSessions() + { + List result = new ArrayList<>(); + result.addAll(getWalletConnectV1SessionItems()); + result.addAll(getWalletConnectV2SessionItems()); + return result; + } + + public void fetchSessions(Context context, SessionFetchCallback sessionFetchCallback) + { + ServiceConnection connection = new ServiceConnection() + { + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + WalletConnectService walletConnectService = ((WalletConnectService.LocalBinder) service).getService(); + fetch(walletConnectService, sessionFetchCallback); + } + + @Override + public void onServiceDisconnected(ComponentName name) + { + } + }; + + WCUtils.startServiceLocal(context, connection, WalletConnectActions.CONNECT); + } + + private void fetch(WalletConnectService walletConnectService, SessionFetchCallback sessionFetchCallback) + { + List result = new ArrayList<>(); + List sessionItems = getWalletConnectV1SessionItems(); + for (WalletConnectSessionItem item : sessionItems) + { + WCClient wcClient = walletConnectService.getClient(item.sessionId); + if (wcClient != null && wcClient.isConnected()) + { + result.add(item); + } + } + + result.addAll(getWalletConnectV2SessionItems()); + sessionFetchCallback.onFetched(result); + } + + private List getWalletConnectV1SessionItems() + { + List sessions = new ArrayList<>(); + try (Realm realm = realmManager.getRealmInstance(WalletConnectViewModel.WC_SESSION_DB)) + { + RealmResults items = realm.where(RealmWCSession.class) + .sort("lastUsageTime", Sort.DESCENDING) + .findAll(); + + for (RealmWCSession r : items) + { + sessions.add(new WalletConnectSessionItem(r)); + } + } + + return sessions; + } + + private List getWalletConnectV2SessionItems() + { + List result = new ArrayList<>(); + try + { + List listOfSettledSessions = SignClient.INSTANCE.getListOfSettledSessions(); + for (Sign.Model.Session session : listOfSettledSessions) + { + result.add(new WalletConnectV2SessionItem(session)); + } + } + catch (IllegalStateException e) + { + Timber.e(e); + } + return result; + } + + public interface SessionFetchCallback + { + void onFetched(List sessions); + } +} + diff --git a/app/src/main/java/com/alphawallet/app/repository/CoinbasePayRepository.java b/app/src/main/java/com/alphawallet/app/repository/CoinbasePayRepository.java new file mode 100644 index 0000000000..a1bf99cf23 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/CoinbasePayRepository.java @@ -0,0 +1,56 @@ +package com.alphawallet.app.repository; + +import android.net.Uri; +import android.text.TextUtils; + +import com.alphawallet.app.entity.coinbasepay.DestinationWallet; +import com.alphawallet.app.util.CoinbasePayUtils; + +import java.util.List; + +public class CoinbasePayRepository implements CoinbasePayRepositoryType +{ + private static final String SCHEME = "https"; + private static final String AUTHORITY = "pay.coinbase.com"; + private static final String BUY_PATH = "buy"; + private static final String SELECT_ASSET_PATH = "select-asset"; + private final KeyProvider keyProvider = KeyProviderFactory.get(); + + @Override + public String getUri(DestinationWallet.Type type, String address, List list) + { + String appId = keyProvider.getCoinbasePayAppId(); + if (TextUtils.isEmpty(appId)) + { + return ""; + } + else + { + Uri.Builder builder = new Uri.Builder(); + builder.scheme(SCHEME) + .authority(AUTHORITY) + .appendPath(BUY_PATH) + .appendPath(SELECT_ASSET_PATH) + .appendQueryParameter(RequestParams.APP_ID, keyProvider.getCoinbasePayAppId()) + .appendQueryParameter(RequestParams.DESTINATION_WALLETS, CoinbasePayUtils.getDestWalletJson(type, address, list)); + + return builder.build().toString(); + } + } + + public static class Blockchains + { + public static final String ETHEREUM = "ethereum"; + public static final String SOLANA = "solana"; + public static final String AVALANCHE_C_CHAIN = "avalanche-c-chain"; + } + + private static class RequestParams + { + public static final String APP_ID = "appId"; + public static final String ADDRESS = "address"; + public static final String DESTINATION_WALLETS = "destinationWallets"; + public static final String ASSETS = "assets"; + public static final String BLOCKCHAINS = "blockchains"; + } +} diff --git a/app/src/main/java/com/alphawallet/app/repository/CoinbasePayRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/CoinbasePayRepositoryType.java new file mode 100644 index 0000000000..f0b8136e13 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/CoinbasePayRepositoryType.java @@ -0,0 +1,9 @@ +package com.alphawallet.app.repository; + +import com.alphawallet.app.entity.coinbasepay.DestinationWallet; + +import java.util.List; + +public interface CoinbasePayRepositoryType { + String getUri(DestinationWallet.Type type, String json, List list); +} diff --git a/app/src/main/java/com/alphawallet/app/repository/CurrencyRepository.java b/app/src/main/java/com/alphawallet/app/repository/CurrencyRepository.java index 80e5ca7c6f..57abe9c760 100644 --- a/app/src/main/java/com/alphawallet/app/repository/CurrencyRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/CurrencyRepository.java @@ -7,6 +7,10 @@ import java.util.Arrays; public class CurrencyRepository implements CurrencyRepositoryType { + /** + * Find currency symbol here: https://coinyep.com/en/currencies + * Find drawables here - https://github.com/Shusshu/android-flags/tree/master/flags/src/main/res/drawable + */ public static final CurrencyItem[] CURRENCIES = { new CurrencyItem("USD", "American Dollar", "$", R.drawable.ic_flags_usa), new CurrencyItem("EUR", "Euro", "€", R.drawable.ic_flags_euro), @@ -19,8 +23,9 @@ public class CurrencyRepository implements CurrencyRepositoryType { new CurrencyItem("KRW", "Korean Won","₩", R.drawable.ic_flags_korea), new CurrencyItem("RUB", "Russian Ruble","₽", R.drawable.ic_flags_russia), new CurrencyItem("VND", "Vietnamese đồng", "₫", R.drawable.ic_flags_vietnam), - new CurrencyItem("PKR", "Pakistani rupee", "Rs", R.drawable.ic_flags_pakistan), - new CurrencyItem("MMK", "Myanmar Kyat", "Ks", R.drawable.ic_flags_myanmar) + new CurrencyItem("PKR", "Pakistani Rupee", "Rs", R.drawable.ic_flags_pakistan), + new CurrencyItem("MMK", "Myanmar Kyat", "Ks", R.drawable.ic_flags_myanmar), + new CurrencyItem("IDR", "Indonesian Rupiah", "Rp", R.drawable.ic_flags_indonesia) }; private final PreferenceRepositoryType preferences; diff --git a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java index 7e3028677e..3a8a658566 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java @@ -3,12 +3,18 @@ /* Please don't add import android at this point. Later this file will be shared * between projects including non-Android projects */ +import static com.alphawallet.app.entity.EventSync.BLOCK_SEARCH_INTERVAL; +import static com.alphawallet.app.entity.EventSync.POLYGON_BLOCK_SEARCH_INTERVAL; +import static com.alphawallet.app.util.Utils.isValidUrl; +import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_GOERLI_TESTNET_FALLBACK_RPC_URL; +import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_GOERLI_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.ARTIS_SIGMA1_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.ARTIS_TAU1_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_MAINNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_MAINNET_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_TESTNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_TESTNET_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; @@ -22,33 +28,37 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.FANTOM_TEST_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.FUJI_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.FUJI_TEST_RPC_URL; +import static com.alphawallet.ethereum.EthereumNetworkBase.GNOSIS_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.GOERLI_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.HECO_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.HECO_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.HECO_TEST_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.HECO_TEST_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.IOTEX_MAINNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.IOTEX_MAINNET_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.IOTEX_TESTNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.IOTEX_TESTNET_RPC_URL; +import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_BAOBAB_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_BAOBAB_RPC; -import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_BOABAB_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_RPC; -import static com.alphawallet.ethereum.EthereumNetworkBase.KOVAN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MILKOMEDA_C1_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MILKOMEDA_C1_RPC; import static com.alphawallet.ethereum.EthereumNetworkBase.MILKOMEDA_C1_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MILKOMEDA_C1_TEST_RPC; +import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISM_GOERLI_TESTNET_FALLBACK_RPC_URL; +import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISM_GOERLI_TEST_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISTIC_MAIN_FALLBACK_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISTIC_MAIN_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISTIC_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.PALM_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.PALM_TEST_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.PHI_NETWORK_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.POA_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.RINKEBY_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.ROPSTEN_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.SOKOL_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_TEST_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.SEPOLIA_TESTNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.SEPOLIA_TESTNET_RPC_URL; +import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_RPC_URL; import android.text.TextUtils; import android.util.LongSparseArray; @@ -70,7 +80,6 @@ import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.response.EthGetTransactionCount; -import org.web3j.protocol.http.HttpService; import java.math.BigDecimal; import java.math.BigInteger; @@ -88,13 +97,9 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy { public static final String COVALENT = "[COVALENT]"; - private static final String DEFAULT_HOMEPAGE = "https://alphawallet.com/browser/"; - - private static final String POLYGON_HOMEPAGE = "https://alphawallet.com/browser-item-category/polygon/"; - private static final String GAS_API = "module=gastracker&action=gasoracle"; - private static final String DEFAULT_INFURA_KEY = "da3717f25f824cc1baa32d812386d93f"; + public static final String DEFAULT_INFURA_KEY = "da3717f25f824cc1baa32d812386d93f"; /* constructing URLs from BuildConfig. In the below area you will see hardcoded key like da3717... These hardcoded keys are fallbacks used by AlphaWallet forks. @@ -103,107 +108,120 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy If you wish your node to be the fallback, tried in case the primary times out then add/replace in ..._FALLBACK_RPC_URL list */ - static { - System.loadLibrary("keys"); - } + private static final KeyProvider keyProvider = KeyProviderFactory.get(); + public static final boolean usesProductionKey = !keyProvider.getInfuraKey().equals(DEFAULT_INFURA_KEY); - public static native String getAmberDataKey(); - public static native String getInfuraKey(); - public static native String getSecondaryInfuraKey(); - private static final boolean usesProductionKey = !getInfuraKey().equals(DEFAULT_INFURA_KEY); - - public static final String FREE_MAINNET_RPC_URL = "https://main-rpc.linkpool.io"; + public static final String FREE_MAINNET_RPC_URL = "https://rpc.ankr.com/eth"; public static final String FREE_POLYGON_RPC_URL = "https://polygon-rpc.com"; public static final String FREE_ARBITRUM_RPC_URL = "https://arbitrum.public-rpc.com"; - public static final String FREE_RINKEBY_RPC_URL = "https://rinkeby-light.eth.linkpool.io"; - public static final String FREE_GOERLI_RPC_URL = "https://goerli-light.eth.linkpool.io"; + public static final String FREE_GOERLI_RPC_URL = "https://rpc.ankr.com/eth_goerli"; public static final String FREE_MUMBAI_RPC_URL = "https://rpc-mumbai.maticvigil.com"; - public static final String FREE_OPTIMISM_RPC_URL = "https://mainnet.optimism.io"; - public static final String FREE_ARBITRUM_TEST_RPC_URL = "https://rinkeby.arbitrum.io/rpc"; - public static final String FREE_KOVAN_RPC_URL = "https://kovan.poa.network"; - public static final String FREE_OPTIMISM_TESTRPC_URL = "https://kovan.optimism.io"; public static final String FREE_PALM_RPC_URL = "https://palm-mainnet.infura.io/v3/3a961d6501e54add9a41aa53f15de99b"; public static final String FREE_PALM_TEST_RPC_URL = "https://palm-testnet.infura.io/v3/3a961d6501e54add9a41aa53f15de99b"; public static final String FREE_CRONOS_MAIN_BETA_RPC_URL = "https://evm.cronos.org"; - public static final String MAINNET_RPC_URL = usesProductionKey ? "https://mainnet.infura.io/v3/" + getInfuraKey() + public static final String MAINNET_RPC_URL = usesProductionKey ? "https://mainnet.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_MAINNET_RPC_URL; - public static final String RINKEBY_RPC_URL = usesProductionKey ? "https://rinkeby.infura.io/v3/" + getInfuraKey() - : FREE_RINKEBY_RPC_URL; - public static final String KOVAN_RPC_URL = usesProductionKey ? "https://kovan.infura.io/v3/" + getInfuraKey() - : FREE_KOVAN_RPC_URL; - public static final String GOERLI_RPC_URL = usesProductionKey ? "https://goerli.infura.io/v3/" + getInfuraKey() + public static final String GOERLI_RPC_URL = usesProductionKey ? "https://goerli.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_GOERLI_RPC_URL; - public static final String MATIC_RPC_URL = usesProductionKey ? "https://polygon-mainnet.infura.io/v3/" + getInfuraKey() + public static final String POLYGON_RPC_URL = usesProductionKey ? "https://polygon-mainnet.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_POLYGON_RPC_URL; - public static final String ARBITRUM_MAINNET_RPC = usesProductionKey ? "https://arbitrum-mainnet.infura.io/v3/" + getInfuraKey() + public static final String ARBITRUM_MAINNET_RPC = usesProductionKey ? "https://arbitrum-mainnet.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_ARBITRUM_RPC_URL; - public static final String MUMBAI_TEST_RPC_URL = usesProductionKey ? "https://polygon-mumbai.infura.io/v3/" + getInfuraKey() + public static final String MUMBAI_TEST_RPC_URL = usesProductionKey ? "https://polygon-mumbai.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_MUMBAI_RPC_URL; - public static final String OPTIMISTIC_MAIN_URL = usesProductionKey ? "https://optimism-mainnet.infura.io/v3/" + getInfuraKey() - : FREE_OPTIMISM_RPC_URL; - public static final String OPTIMISTIC_TEST_URL = usesProductionKey ? "https://optimism-kovan.infura.io/v3/" + getInfuraKey() - : FREE_OPTIMISM_TESTRPC_URL; - public static final String ARBITRUM_TESTNET_RPC = usesProductionKey ? "https://arbitrum-rinkeby.infura.io/v3/" + getInfuraKey() - : FREE_ARBITRUM_TEST_RPC_URL; - public static final String PALM_RPC_URL = usesProductionKey ? "https://palm-mainnet.infura.io/v3/" + getInfuraKey() + public static final String OPTIMISTIC_MAIN_URL = usesProductionKey ? "https://optimism-mainnet.infura.io/v3/" + keyProvider.getInfuraKey() + : OPTIMISTIC_MAIN_FALLBACK_URL; + public static final String PALM_RPC_URL = usesProductionKey ? "https://palm-mainnet.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_PALM_RPC_URL; - public static final String PALM_TEST_RPC_URL = usesProductionKey ? "https://palm-testnet.infura.io/v3/" + getInfuraKey() + public static final String PALM_TEST_RPC_URL = usesProductionKey ? "https://palm-testnet.infura.io/v3/" + keyProvider.getInfuraKey() : FREE_PALM_TEST_RPC_URL; + public static final String USE_KLAYTN_RPC = usesProductionKey ? "https://node-api.klaytnapi.com/v1/klaytn" + : KLAYTN_RPC; + public static final String USE_KLAYTN_BAOBAB_RPC = usesProductionKey ? "https://node-api.klaytnapi.com/v1/klaytn" + : KLAYTN_BAOBAB_RPC; public static final String CRONOS_MAIN_RPC_URL = "https://evm.cronos.org"; // Use the "Free" routes as backup in order to diversify node usage; to avoid single point of failure - public static final String MAINNET_FALLBACK_RPC_URL = usesProductionKey ? FREE_MAINNET_RPC_URL : "https://mainnet.infura.io/v3/" + getSecondaryInfuraKey(); - public static final String RINKEBY_FALLBACK_RPC_URL = usesProductionKey ? FREE_RINKEBY_RPC_URL : "https://rinkeby.infura.io/v3/" + getSecondaryInfuraKey(); - public static final String KOVAN_FALLBACK_RPC_URL = usesProductionKey ? FREE_KOVAN_RPC_URL : "https://kovan.infura.io/v3/" + getSecondaryInfuraKey(); - public static final String GOERLI_FALLBACK_RPC_URL = usesProductionKey ? FREE_GOERLI_RPC_URL : "https://goerli.infura.io/v3/" + getSecondaryInfuraKey(); - public static final String ARBITRUM_FALLBACK_MAINNET_RPC = usesProductionKey ? FREE_ARBITRUM_RPC_URL : "https://arbitrum-mainnet.infura.io/v3/" + getSecondaryInfuraKey(); - public static final String PALM_RPC_FALLBACK_URL = usesProductionKey ? FREE_PALM_RPC_URL : "https://palm-mainnet.infura.io/v3/" + getSecondaryInfuraKey(); - public static final String PALM_TEST_RPC_FALLBACK_URL = usesProductionKey ? FREE_PALM_RPC_URL : "https://palm-testnet.infura.io/v3/" + getSecondaryInfuraKey(); + public static final String MAINNET_FALLBACK_RPC_URL = usesProductionKey ? FREE_MAINNET_RPC_URL : "https://mainnet.infura.io/v3/" + keyProvider.getSecondaryInfuraKey(); + public static final String GOERLI_FALLBACK_RPC_URL = usesProductionKey ? FREE_GOERLI_RPC_URL : "https://goerli.infura.io/v3/" + keyProvider.getSecondaryInfuraKey(); + public static final String ARBITRUM_FALLBACK_MAINNET_RPC = usesProductionKey ? FREE_ARBITRUM_RPC_URL : "https://arbitrum-mainnet.infura.io/v3/" + keyProvider.getSecondaryInfuraKey(); + public static final String PALM_RPC_FALLBACK_URL = usesProductionKey ? FREE_PALM_RPC_URL : "https://palm-mainnet.infura.io/v3/" + keyProvider.getSecondaryInfuraKey(); + public static final String PALM_TEST_RPC_FALLBACK_URL = usesProductionKey ? FREE_PALM_RPC_URL : "https://palm-testnet.infura.io/v3/" + keyProvider.getSecondaryInfuraKey(); //Note that AlphaWallet now uses a double node configuration. See class AWHttpService comment 'try primary node'. //If you supply a main RPC and secondary it will try the secondary if the primary node times out after 10 seconds. //See the declaration of NetworkInfo - it has a member backupNodeUrl. Put your secondary node here. - public static final String ROPSTEN_FALLBACK_RPC_URL = "https://ropsten.infura.io/v3/" + getSecondaryInfuraKey(); public static final String CLASSIC_RPC_URL = "https://www.ethercluster.com/etc"; - public static final String XDAI_RPC_URL = com.alphawallet.ethereum.EthereumNetworkBase.XDAI_RPC_URL; public static final String POA_RPC_URL = "https://core.poa.network/"; - public static final String ROPSTEN_RPC_URL = "https://ropsten.infura.io/v3/" + getInfuraKey(); - public static final String SOKOL_RPC_URL = "https://sokol.poa.network"; public static final String ARTIS_SIGMA1_RPC_URL = "https://rpc.sigma1.artis.network"; public static final String ARTIS_TAU1_RPC_URL = "https://rpc.tau1.artis.network"; public static final String BINANCE_TEST_RPC_URL = "https://data-seed-prebsc-1-s3.binance.org:8545"; public static final String BINANCE_TEST_FALLBACK_RPC_URL = "https://data-seed-prebsc-2-s1.binance.org:8545"; public static final String BINANCE_MAIN_RPC_URL = "https://bsc-dataseed.binance.org"; public static final String BINANCE_MAIN_FALLBACK_RPC_URL = "https://bsc-dataseed2.ninicoin.io:443"; - public static final String HECO_RPC_URL = "https://http-mainnet.hecochain.com"; - public static final String HECO_TEST_RPC_URL = "https://http-testnet.hecochain.com"; - public static final String MATIC_FALLBACK_RPC_URL = "https://matic-mainnet.chainstacklabs.com"; + public static final String POLYGON_FALLBACK_RPC_URL = "https://matic-mainnet.chainstacklabs.com"; public static final String MUMBAI_FALLBACK_RPC_URL = "https://matic-mumbai.chainstacklabs.com"; - public static final String OPTIMISTIC_MAIN_FALLBACK_URL = "https://mainnet.optimism.io"; - public static final String OPTIMISTIC_TEST_FALLBACK_URL = "https://kovan.optimism.io"; public static final String CRONOS_TEST_URL = "https://evm-t3.cronos.org"; public static final String ARBITRUM_FALLBACK_TESTNET_RPC = "https://rinkeby.arbitrum.io/rpc"; - public static final String IOTEX_MAINNET_RPC_URL = "https://babel-api.mainnet.iotex.io"; public static final String IOTEX_MAINNET_RPC_FALLBACK_URL = "https://rpc.ankr.com/iotex"; - public static final String IOTEX_TESTNET_RPC_URL = "https://babel-api.testnet.iotex.io"; - public static final String AURORA_MAINNET_RPC_URL = "https://mainnet.aurora.dev"; - public static final String AURORA_TESTNET_RPC_URL = "https://testnet.aurora.dev"; - public static final String PHI_NETWORK_RPC = "https://rpc1.phi.network"; + public static final String OPTIMISM_GOERLI_TESTNET_RPC_URL = "https://optimism-goerli.infura.io/v3/" + keyProvider.getInfuraKey(); + public static final String ARBITRUM_GOERLI_TESTNET_RPC_URL = "https://arbitrum-goerli.infura.io/v3/" + keyProvider.getInfuraKey(); //All chains that have fiat/real value (not testnet) must be put here //Note: This list also determines the order of display for main net chains in the wallet. //If your wallet prioritises xDai for example, you may want to move the XDAI_ID to the front of this list, //Then xDai would appear as the first token at the top of the wallet private static final List hasValue = new ArrayList<>(Arrays.asList( - MAINNET_ID, CLASSIC_ID, XDAI_ID, POA_ID, ARTIS_SIGMA1_ID, BINANCE_MAIN_ID, HECO_ID, AVALANCHE_ID, - FANTOM_ID, MATIC_ID, OPTIMISTIC_MAIN_ID, CRONOS_MAIN_ID, ARBITRUM_MAIN_ID, PALM_ID, KLAYTN_ID, IOTEX_MAINNET_ID, AURORA_MAINNET_ID, MILKOMEDA_C1_ID, - PHI_NETWORK_MAIN_ID)); + MAINNET_ID, GNOSIS_ID, POLYGON_ID, CLASSIC_ID, POA_ID, ARTIS_SIGMA1_ID, BINANCE_MAIN_ID, HECO_ID, AVALANCHE_ID, + FANTOM_ID, OPTIMISTIC_MAIN_ID, CRONOS_MAIN_ID, ARBITRUM_MAIN_ID, PALM_ID, KLAYTN_ID, IOTEX_MAINNET_ID, AURORA_MAINNET_ID, MILKOMEDA_C1_ID)); + + private static final List testnetList = new ArrayList<>(Arrays.asList( + GOERLI_ID, BINANCE_TEST_ID, HECO_TEST_ID, CRONOS_TEST_ID, OPTIMISM_GOERLI_TEST_ID, ARBITRUM_GOERLI_TEST_ID, KLAYTN_BAOBAB_ID, + FANTOM_TEST_ID, IOTEX_TESTNET_ID, FUJI_TEST_ID, POLYGON_TEST_ID, MILKOMEDA_C1_TEST_ID, ARTIS_TAU1_ID, + SEPOLIA_TESTNET_ID, AURORA_TESTNET_ID, PALM_TEST_ID)); + + private static final List deprecatedNetworkList = new ArrayList<>(Arrays.asList( + // Add deprecated testnet IDs here + )); + + private static final String INFURA_ENDPOINT = ".infura.io/v3/"; + + @Override + public String getDappBrowserRPC(long chainId) + { + NetworkInfo info = getNetworkByChain(chainId); + + if (info == null) + { + return ""; + } + + int index = info.rpcServerUrl.indexOf(INFURA_ENDPOINT); + if (index > 0) + { + return info.rpcServerUrl.substring(0, index + INFURA_ENDPOINT.length()) + keyProvider.getTertiaryInfuraKey(); + } + else if (info.backupNodeUrl != null) + { + return info.backupNodeUrl; + } + else + { + return info.rpcServerUrl; + } + } + + public static boolean isInfura(String rpcServerUrl) + { + return rpcServerUrl.contains(INFURA_ENDPOINT); + } // for reset built-in network - private static final LongSparseArray builtinNetworkMap = new LongSparseArray() { + private static final LongSparseArray builtinNetworkMap = new LongSparseArray() + { { put(MAINNET_ID, new NetworkInfo(C.ETHEREUM_NETWORK_NAME, C.ETH_SYMBOL, MAINNET_RPC_URL, @@ -213,10 +231,10 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy CLASSIC_RPC_URL, "https://blockscout.com/etc/mainnet/tx/", CLASSIC_ID, CLASSIC_RPC_URL, "https://blockscout.com/etc/mainnet/api?")); - put(XDAI_ID, new NetworkInfo(C.XDAI_NETWORK_NAME, C.xDAI_SYMBOL, + put(GNOSIS_ID, new NetworkInfo(C.XDAI_NETWORK_NAME, C.xDAI_SYMBOL, XDAI_RPC_URL, - "https://blockscout.com/xdai/mainnet/tx/", XDAI_ID, - "https://gnosis.public-rpc.com", "https://blockscout.com/xdai/mainnet/api?")); + "https://blockscout.com/xdai/mainnet/tx/", GNOSIS_ID, + "https://rpc.ankr.com/gnosis", "https://blockscout.com/xdai/mainnet/api?")); put(POA_ID, new NetworkInfo(C.POA_NETWORK_NAME, C.POA_SYMBOL, POA_RPC_URL, "https://blockscout.com/poa/core/tx/", POA_ID, POA_RPC_URL, @@ -225,22 +243,6 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy ARTIS_SIGMA1_RPC_URL, "https://explorer.sigma1.artis.network/tx/", ARTIS_SIGMA1_ID, ARTIS_SIGMA1_RPC_URL, "https://explorer.sigma1.artis.network/api?")); - put(KOVAN_ID, new NetworkInfo(C.KOVAN_NETWORK_NAME, C.ETH_SYMBOL, - KOVAN_RPC_URL, - "https://kovan.etherscan.io/tx/", KOVAN_ID, - KOVAN_FALLBACK_RPC_URL, "https://api-kovan.etherscan.io/api?")); - put(ROPSTEN_ID, new NetworkInfo(C.ROPSTEN_NETWORK_NAME, C.ETH_SYMBOL, - ROPSTEN_RPC_URL, - "https://ropsten.etherscan.io/tx/", ROPSTEN_ID, - ROPSTEN_FALLBACK_RPC_URL, "https://api-ropsten.etherscan.io/api?")); - put(SOKOL_ID, new NetworkInfo(C.SOKOL_NETWORK_NAME, C.POA_SYMBOL, - SOKOL_RPC_URL, - "https://blockscout.com/poa/sokol/tx/", SOKOL_ID, - SOKOL_RPC_URL, "https://blockscout.com/poa/sokol/api?")); - put(RINKEBY_ID, new NetworkInfo(C.RINKEBY_NETWORK_NAME, C.ETH_SYMBOL, - RINKEBY_RPC_URL, - "https://rinkeby.etherscan.io/tx/", RINKEBY_ID, - RINKEBY_FALLBACK_RPC_URL, "https://api-rinkeby.etherscan.io/api?")); put(GOERLI_ID, new NetworkInfo(C.GOERLI_NETWORK_NAME, C.GOERLI_SYMBOL, GOERLI_RPC_URL, "https://goerli.etherscan.io/tx/", GOERLI_ID, @@ -281,21 +283,17 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy FANTOM_TEST_RPC_URL, "https://explorer.testnet.fantom.network/tx/", FANTOM_TEST_ID, FANTOM_TEST_RPC_URL, "https://api.covalenthq.com/v1/" + COVALENT)); //NB: Fantom testnet not yet supported by Covalent - put(MATIC_ID, new NetworkInfo(C.MATIC_NETWORK, C.MATIC_SYMBOL, MATIC_RPC_URL, - "https://polygonscan.com/tx/", MATIC_ID, - MATIC_FALLBACK_RPC_URL, "https://api.polygonscan.com/api?")); - put(MATIC_TEST_ID, new NetworkInfo(C.MATIC_TEST_NETWORK, C.MATIC_SYMBOL, + put(POLYGON_ID, new NetworkInfo(C.POLYGON_NETWORK, C.POLYGON_SYMBOL, POLYGON_RPC_URL, + "https://polygonscan.com/tx/", POLYGON_ID, + POLYGON_FALLBACK_RPC_URL, "https://api.polygonscan.com/api?")); + put(POLYGON_TEST_ID, new NetworkInfo(C.POLYGON_TEST_NETWORK, C.POLYGON_SYMBOL, MUMBAI_TEST_RPC_URL, - "https://mumbai.polygonscan.com/tx/", MATIC_TEST_ID, + "https://mumbai.polygonscan.com/tx/", POLYGON_TEST_ID, MUMBAI_FALLBACK_RPC_URL, " https://api-testnet.polygonscan.com/api?")); put(OPTIMISTIC_MAIN_ID, new NetworkInfo(C.OPTIMISTIC_NETWORK, C.ETH_SYMBOL, OPTIMISTIC_MAIN_URL, "https://optimistic.etherscan.io/tx/", OPTIMISTIC_MAIN_ID, OPTIMISTIC_MAIN_FALLBACK_URL, "https://api-optimistic.etherscan.io/api?")); - put(OPTIMISTIC_TEST_ID, new NetworkInfo(C.OPTIMISTIC_TEST_NETWORK, C.ETH_SYMBOL, - OPTIMISTIC_TEST_URL, - "https://kovan-optimistic.etherscan.io/tx/", OPTIMISTIC_TEST_ID, OPTIMISTIC_TEST_FALLBACK_URL, - "https://api-kovan-optimistic.etherscan.io/api?")); put(CRONOS_MAIN_ID, new NetworkInfo(C.CRONOS_MAIN_NETWORK, C.CRONOS_SYMBOL, CRONOS_MAIN_RPC_URL, "https://cronoscan.com/tx/", CRONOS_MAIN_ID, CRONOS_MAIN_RPC_URL, @@ -308,10 +306,6 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy ARBITRUM_MAINNET_RPC, "https://arbiscan.io/tx/", ARBITRUM_MAIN_ID, ARBITRUM_FALLBACK_MAINNET_RPC, "https://api.arbiscan.io/api?")); - put(ARBITRUM_TEST_ID, new NetworkInfo(C.ARBITRUM_TEST_NETWORK, C.ARBITRUM_TEST_SYMBOL, - ARBITRUM_TESTNET_RPC, - "https://testnet.arbiscan.io/tx/", ARBITRUM_TEST_ID, ARBITRUM_FALLBACK_TESTNET_RPC, - "https://testnet.arbiscan.io/api?")); //no transaction API put(PALM_ID, new NetworkInfo(C.PALM_NAME, C.PALM_SYMBOL, PALM_RPC_URL, "https://explorer.palm.io/tx/", PALM_ID, PALM_RPC_FALLBACK_URL, @@ -320,14 +314,13 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy PALM_TEST_RPC_URL, "https://explorer.palm-uat.xyz/tx/", PALM_TEST_ID, PALM_TEST_RPC_FALLBACK_URL, "https://explorer.palm-uat.xyz/api?")); - put(KLAYTN_ID, new NetworkInfo(C.KLAYTN_NAME, C.KLAYTN_SYMBOL, - KLAYTN_RPC, - "https://scope.klaytn.com/tx/", KLAYTN_ID, "", + USE_KLAYTN_RPC, + "https://scope.klaytn.com/tx/", KLAYTN_ID, KLAYTN_RPC, "https://api.covalenthq.com/v1/" + COVALENT)); - put(KLAYTN_BOABAB_ID, new NetworkInfo(C.KLAYTN_BAOBAB_NAME, C.KLAYTN_SYMBOL, - KLAYTN_BAOBAB_RPC, - "https://baobab.scope.klaytn.com/tx/", KLAYTN_BOABAB_ID, "", + put(KLAYTN_BAOBAB_ID, new NetworkInfo(C.KLAYTN_BAOBAB_NAME, C.KLAYTN_SYMBOL, + USE_KLAYTN_BAOBAB_RPC, + "https://baobab.scope.klaytn.com/tx/", KLAYTN_BAOBAB_ID, KLAYTN_BAOBAB_RPC, "")); put(IOTEX_MAINNET_ID, new NetworkInfo(C.IOTEX_NAME, C.IOTEX_SYMBOL, IOTEX_MAINNET_RPC_URL, @@ -343,7 +336,6 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy put(AURORA_TESTNET_ID, new NetworkInfo(C.AURORA_TESTNET_NAME, C.ETH_SYMBOL, AURORA_TESTNET_RPC_URL, "https://testnet.aurorascan.dev/tx/", AURORA_TESTNET_ID, "", "https://api-testnet.aurorascan.dev/api?")); - put(MILKOMEDA_C1_ID, new NetworkInfo(C.MILKOMEDA_NAME, C.MILKOMEDA_SYMBOL, MILKOMEDA_C1_RPC, "https://explorer-mainnet-cardano-evm.c1.milkomeda.com/tx/", MILKOMEDA_C1_ID, "", @@ -352,10 +344,20 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy MILKOMEDA_C1_TEST_RPC, "https://explorer-devnet-cardano-evm.c1.milkomeda.com/tx/", MILKOMEDA_C1_TEST_ID, "", "https://explorer-devnet-cardano-evm.c1.milkomeda.com/api?")); - put(PHI_NETWORK_MAIN_ID, new NetworkInfo(C.PHI_NETWORK_NAME, C.PHI_NETWORK_SYMBOL, - PHI_NETWORK_RPC, - "https://explorer.phi.network/tx/", PHI_NETWORK_MAIN_ID, "https://rpc2.phi.network", - "")); + put(SEPOLIA_TESTNET_ID, new NetworkInfo(C.SEPOLIA_TESTNET_NAME, C.SEPOLIA_SYMBOL, + SEPOLIA_TESTNET_RPC_URL, + "https://sepolia.etherscan.io/tx/", SEPOLIA_TESTNET_ID, "https://rpc2.sepolia.org", + "https://api-sepolia.etherscan.io/api?")); + put(OPTIMISM_GOERLI_TEST_ID, new NetworkInfo(C.OPTIMISM_GOERLI_TESTNET_NAME, C.OPTIMISM_GOERLI_TEST_SYMBOL, + OPTIMISM_GOERLI_TESTNET_RPC_URL, + "https://blockscout.com/optimism/goerli/tx/", OPTIMISM_GOERLI_TEST_ID, OPTIMISM_GOERLI_TESTNET_FALLBACK_RPC_URL, + "https://blockscout.com/optimism/goerli/api?")); + put(ARBITRUM_GOERLI_TEST_ID, new NetworkInfo(C.ARBITRUM_GOERLI_TESTNET_NAME, C.ARBITRUM_SYMBOL, + ARBITRUM_GOERLI_TESTNET_RPC_URL, + "https://goerli-rollup-explorer.arbitrum.io/tx/", ARBITRUM_GOERLI_TEST_ID, ARBITRUM_GOERLI_TESTNET_FALLBACK_RPC_URL, + "https://goerli-rollup-explorer.arbitrum.io/api?")); + + // Add deprecated networks after this line } }; @@ -363,16 +365,13 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy //the entries are automatically sorted into numerical order private static final LongSparseArray networkMap = builtinNetworkMap.clone(); - private static final LongSparseArray chainLogos = new LongSparseArray() { + private static final LongSparseArray chainLogos = new LongSparseArray() + { { put(MAINNET_ID, R.drawable.ic_token_eth); - put(KOVAN_ID, R.drawable.ic_kovan); - put(ROPSTEN_ID, R.drawable.ic_ropsten); - put(RINKEBY_ID, R.drawable.ic_rinkeby); put(CLASSIC_ID, R.drawable.ic_icons_network_etc); //classic_logo put(POA_ID, R.drawable.ic_poa_logo); - put(SOKOL_ID, R.drawable.ic_icons_tokens_sokol); - put(XDAI_ID, R.drawable.ic_icons_network_gnosis); + put(GNOSIS_ID, R.drawable.ic_icons_network_gnosis); put(GOERLI_ID, R.drawable.ic_goerli); put(ARTIS_SIGMA1_ID, R.drawable.ic_artis_sigma_logo); put(ARTIS_TAU1_ID, R.drawable.ic_artis_tau_logo); @@ -384,38 +383,35 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy put(FANTOM_TEST_ID, R.drawable.ic_icons_fantom_test); put(AVALANCHE_ID, R.drawable.ic_icons_tokens_avalanche); put(FUJI_TEST_ID, R.drawable.ic_icons_tokens_avalanche_testnet); - put(MATIC_ID, R.drawable.ic_icons_polygon); - put(MATIC_TEST_ID, R.drawable.ic_icons_tokens_mumbai); + put(POLYGON_ID, R.drawable.ic_icons_polygon); + put(POLYGON_TEST_ID, R.drawable.ic_icons_tokens_mumbai); put(OPTIMISTIC_MAIN_ID, R.drawable.ic_optimism_logo); - put(OPTIMISTIC_TEST_ID, R.drawable.ic_optimism_testnet_logo); put(CRONOS_MAIN_ID, R.drawable.ic_cronos_mainnet); put(CRONOS_TEST_ID, R.drawable.ic_cronos); put(ARBITRUM_MAIN_ID, R.drawable.ic_icons_arbitrum); - put(ARBITRUM_TEST_ID, R.drawable.ic_icons_arbitrum_test); put(PALM_ID, R.drawable.ic_icons_network_palm); put(PALM_TEST_ID, R.drawable.palm_logo_test); put(KLAYTN_ID, R.drawable.ic_klaytn_network_logo); - put(KLAYTN_BOABAB_ID, R.drawable.ic_klaytn_test); + put(KLAYTN_BAOBAB_ID, R.drawable.ic_klaytn_test); put(IOTEX_MAINNET_ID, R.drawable.ic_iotex); put(IOTEX_TESTNET_ID, R.drawable.ic_iotex_test); put(AURORA_MAINNET_ID, R.drawable.ic_aurora); put(AURORA_TESTNET_ID, R.drawable.ic_aurora_test); put(MILKOMEDA_C1_ID, R.drawable.ic_milkomeda); put(MILKOMEDA_C1_TEST_ID, R.drawable.ic_milkomeda_test); - put(PHI_NETWORK_MAIN_ID, R.drawable.ic_phi_network); + put(SEPOLIA_TESTNET_ID, R.drawable.ic_sepolia_test); + put(OPTIMISM_GOERLI_TEST_ID, R.drawable.ic_optimism_testnet_logo); + put(ARBITRUM_GOERLI_TEST_ID, R.drawable.ic_icons_arbitrum_test); } }; - private static final LongSparseArray smallChainLogos = new LongSparseArray() { + private static final LongSparseArray smallChainLogos = new LongSparseArray() + { { put(MAINNET_ID, R.drawable.ic_icons_network_eth); - put(KOVAN_ID, R.drawable.ic_kovan); - put(ROPSTEN_ID, R.drawable.ic_ropsten); - put(RINKEBY_ID, R.drawable.ic_rinkeby); put(CLASSIC_ID, R.drawable.ic_icons_network_etc); put(POA_ID, R.drawable.ic_icons_network_poa); - put(SOKOL_ID, R.drawable.ic_icons_tokens_sokol); - put(XDAI_ID, R.drawable.ic_icons_network_gnosis); + put(GNOSIS_ID, R.drawable.ic_icons_network_gnosis); put(GOERLI_ID, R.drawable.ic_goerli); put(ARTIS_SIGMA1_ID, R.drawable.ic_icons_network_artis); put(ARTIS_TAU1_ID, R.drawable.ic_artis_tau_logo); @@ -427,38 +423,35 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy put(FANTOM_TEST_ID, R.drawable.ic_icons_fantom_test); put(AVALANCHE_ID, R.drawable.ic_icons_network_avalanche); put(FUJI_TEST_ID, R.drawable.ic_icons_tokens_avalanche_testnet); - put(MATIC_ID, R.drawable.ic_icons_network_polygon); - put(MATIC_TEST_ID, R.drawable.ic_icons_tokens_mumbai); + put(POLYGON_ID, R.drawable.ic_icons_network_polygon); + put(POLYGON_TEST_ID, R.drawable.ic_icons_tokens_mumbai); put(OPTIMISTIC_MAIN_ID, R.drawable.ic_icons_network_optimism); - put(OPTIMISTIC_TEST_ID, R.drawable.ic_optimism_testnet_logo); put(CRONOS_MAIN_ID, R.drawable.ic_cronos_mainnet); put(CRONOS_TEST_ID, R.drawable.ic_cronos); put(ARBITRUM_MAIN_ID, R.drawable.ic_icons_network_arbitrum); - put(ARBITRUM_TEST_ID, R.drawable.ic_icons_arbitrum_test); put(PALM_ID, R.drawable.ic_icons_network_palm); put(PALM_TEST_ID, R.drawable.palm_logo_test); put(KLAYTN_ID, R.drawable.ic_klaytn_network_logo); - put(KLAYTN_BOABAB_ID, R.drawable.ic_klaytn_test); + put(KLAYTN_BAOBAB_ID, R.drawable.ic_klaytn_test); put(IOTEX_MAINNET_ID, R.drawable.ic_iotex); put(IOTEX_TESTNET_ID, R.drawable.ic_iotex_test); put(AURORA_MAINNET_ID, R.drawable.ic_aurora); put(AURORA_TESTNET_ID, R.drawable.ic_aurora_test); put(MILKOMEDA_C1_ID, R.drawable.ic_milkomeda); put(MILKOMEDA_C1_TEST_ID, R.drawable.ic_milkomeda_test); - put(PHI_NETWORK_MAIN_ID, R.drawable.ic_phi_network); + put(SEPOLIA_TESTNET_ID, R.drawable.ic_sepolia_test); + put(OPTIMISM_GOERLI_TEST_ID, R.drawable.ic_optimism_testnet_logo); + put(ARBITRUM_GOERLI_TEST_ID, R.drawable.ic_icons_arbitrum_test); } }; - private static final LongSparseArray chainColours = new LongSparseArray() { + private static final LongSparseArray chainColours = new LongSparseArray() + { { put(MAINNET_ID, R.color.mainnet); - put(KOVAN_ID, R.color.kovan); - put(ROPSTEN_ID, R.color.ropsten); - put(RINKEBY_ID, R.color.rinkeby); put(CLASSIC_ID, R.color.classic); put(POA_ID, R.color.poa); - put(SOKOL_ID, R.color.sokol); - put(XDAI_ID, R.color.xdai); + put(GNOSIS_ID, R.color.xdai); put(GOERLI_ID, R.color.goerli); put(ARTIS_SIGMA1_ID, R.color.artis_sigma1); put(ARTIS_TAU1_ID, R.color.artis_tau1); @@ -470,25 +463,25 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy put(FANTOM_TEST_ID, R.color.fantom_test); put(AVALANCHE_ID, R.color.avalanche_main); put(FUJI_TEST_ID, R.color.avalanche_test); - put(MATIC_ID, R.color.polygon_main); - put(MATIC_TEST_ID, R.color.polygon_test); + put(POLYGON_ID, R.color.polygon_main); + put(POLYGON_TEST_ID, R.color.polygon_test); put(OPTIMISTIC_MAIN_ID, R.color.optimistic_main); - put(OPTIMISTIC_TEST_ID, R.color.optimistic_test); put(CRONOS_MAIN_ID, R.color.cronos_main); put(CRONOS_TEST_ID, R.color.cronos_test); put(ARBITRUM_MAIN_ID, R.color.arbitrum_main); - put(ARBITRUM_TEST_ID, R.color.arbitrum_test); put(PALM_ID, R.color.palm_main); put(PALM_TEST_ID, R.color.palm_test); put(KLAYTN_ID, R.color.klaytn_main); - put(KLAYTN_BOABAB_ID, R.color.klaytn_test); + put(KLAYTN_BAOBAB_ID, R.color.klaytn_test); put(IOTEX_MAINNET_ID, R.color.iotex_mainnet); put(IOTEX_TESTNET_ID, R.color.iotex_mainnet); put(AURORA_MAINNET_ID, R.color.aurora_mainnet); put(AURORA_TESTNET_ID, R.color.aurora_testnet); put(MILKOMEDA_C1_ID, R.color.milkomeda); put(MILKOMEDA_C1_TEST_ID, R.color.milkomeda_test); - put(PHI_NETWORK_MAIN_ID, R.color.phi_network); + put(SEPOLIA_TESTNET_ID, R.color.sepolia); + put(OPTIMISM_GOERLI_TEST_ID, R.color.optimistic_test); + put(ARBITRUM_GOERLI_TEST_ID, R.color.arbitrum_test); } }; @@ -496,12 +489,12 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy //Add it to this list here if so. Note that so far, all gas oracles follow the same format: // + GAS_API //If the gas oracle you're adding doesn't follow this spec then you'll have to change the getGasOracle method - private static final List hasGasOracleAPI = Arrays.asList(MAINNET_ID, HECO_ID, BINANCE_MAIN_ID, MATIC_ID); + private static final List hasGasOracleAPI = Arrays.asList(MAINNET_ID, HECO_ID, BINANCE_MAIN_ID, POLYGON_ID); //These chains don't allow custom gas - private static final List hasLockedGas = Arrays.asList(OPTIMISTIC_MAIN_ID, OPTIMISTIC_TEST_ID, ARBITRUM_MAIN_ID, ARBITRUM_TEST_ID, KLAYTN_ID, KLAYTN_BOABAB_ID); + private static final List hasLockedGas = Arrays.asList(OPTIMISTIC_MAIN_ID, ARBITRUM_MAIN_ID, KLAYTN_ID, KLAYTN_BAOBAB_ID); - private static final List hasOpenSeaAPI = Arrays.asList(MAINNET_ID, MATIC_ID, RINKEBY_ID); + private static final List hasOpenSeaAPI = Arrays.asList(MAINNET_ID, POLYGON_ID, ARBITRUM_GOERLI_TEST_ID, AVALANCHE_ID, KLAYTN_ID, OPTIMISM_GOERLI_TEST_ID, GOERLI_ID); private static final LongSparseArray blockGasLimit = new LongSparseArray() { @@ -536,20 +529,68 @@ else if (networkMap.indexOfKey(chainId) >= 0) } else { - return 500 + (int)chainId%500; //fixed ID above 500 + return 500 + (int) chainId % 500; //fixed ID above 500 + } + } + + public static final int INFURA_BATCH_LIMIT = 512; + public static final String INFURA_DOMAIN = "infura.io"; + + //TODO: Refactor when we bump the version of java to allow switch on Long (Finally!!) + //Also TODO: add a test to check these batch limits of each chain we support + private static int batchProcessingLimit(long chainId) + { + NetworkInfo info = builtinNetworkMap.get(chainId); + if (info.rpcServerUrl.contains(INFURA_DOMAIN)) //infura supported chains can handle tx batches of 1000 and up + { + return INFURA_BATCH_LIMIT; + } + else if (info.rpcServerUrl.contains("klaytn") || info.rpcServerUrl.contains("rpc.ankr.com")) + { + return 0; + } + else if (chainId == GNOSIS_ID) + { + return 6; //TODO: Check limit: + } + else if (info.rpcServerUrl.contains("cronos.org")) + { + return 5; //TODO: Check limit + } + else + { + return 32; + } + } + + private static final LongSparseArray batchProcessingLimitMap = new LongSparseArray<>(); + + //Init the batch limits + private static void setBatchProcessingLimits() + { + for (int i = 0; i < builtinNetworkMap.size(); i++) + { + NetworkInfo info = builtinNetworkMap.valueAt(i); + batchProcessingLimitMap.put(info.chainId, batchProcessingLimit(info.chainId)); } } + public static int getBatchProcessingLimit(long chainId) + { + if (batchProcessingLimitMap.size() == 0) setBatchProcessingLimits(); //If batch limits not set, init them and proceed + return batchProcessingLimitMap.get(chainId, 0); //default to zero / no batching + } + @Override public boolean hasLockedGas(long chainId) { return hasLockedGas.contains(chainId); } - - static final Map addressOverride = new HashMap() { + + static final Map addressOverride = new HashMap() + { { put(OPTIMISTIC_MAIN_ID, "0x4200000000000000000000000000000000000006"); - put(OPTIMISTIC_TEST_ID, "0x4200000000000000000000000000000000000006"); } }; @@ -559,40 +600,58 @@ public boolean hasLockedGas(long chainId) final NetworkInfo[] additionalNetworks; - static class CustomNetworks { + static class CustomNetworks + { private ArrayList list = new ArrayList<>(); private Map mapToTestNet = new HashMap<>(); final transient private PreferenceRepositoryType preferences; - public CustomNetworks(PreferenceRepositoryType preferences) { + public CustomNetworks(PreferenceRepositoryType preferences) + { this.preferences = preferences; restore(); } - public void restore() { + public void restore() + { String networks = preferences.getCustomRPCNetworks(); - if (!TextUtils.isEmpty(networks)) { + if (!TextUtils.isEmpty(networks)) + { CustomNetworks cn = new Gson().fromJson(networks, CustomNetworks.class); this.list = cn.list; this.mapToTestNet = cn.mapToTestNet; checkCustomNetworkSetting(); - for (NetworkInfo info : list) { + for (NetworkInfo info : list) + { + if (!isValidUrl(info.rpcServerUrl)) //ensure RPC doesn't contain malicious code + { + continue; + } + networkMap.put(info.chainId, info); Boolean value = mapToTestNet.get(info.chainId); boolean isTestnet = value != null && value; - if (!isTestnet && !hasValue.contains(info.chainId)) { + if (!isTestnet && !hasValue.contains(info.chainId)) + { hasValue.add(info.chainId); } + else if (isTestnet && !testnetList.contains(info.chainId)) + { + testnetList.add(info.chainId); + } } } } - private void checkCustomNetworkSetting() { - if (list.size() > 0 && !list.get(0).isCustom) { //need to update the list + private void checkCustomNetworkSetting() + { + if (list.size() > 0 && !list.get(0).isCustom) + { //need to update the list List copyList = new ArrayList<>(list); list.clear(); - for (NetworkInfo n : copyList) { + for (NetworkInfo n : copyList) + { boolean isCustom = builtinNetworkMap.indexOfKey(n.chainId) == -1; NetworkInfo newInfo = new NetworkInfo(n.name, n.symbol, n.rpcServerUrl, n.etherscanUrl, n.chainId, n.backupNodeUrl, n.etherscanAPI, isCustom); list.add(newInfo); @@ -604,9 +663,12 @@ private void checkCustomNetworkSetting() { public void save(NetworkInfo info, boolean isTestnet, Long oldChainId) { - if (oldChainId != null) { + if (oldChainId != null) + { updateNetwork(info, isTestnet, oldChainId); - } else { + } + else + { addNetwork(info, isTestnet); } @@ -618,11 +680,14 @@ private void updateNetwork(NetworkInfo info, boolean isTestnet, long oldChainId) { removeNetwork(oldChainId); list.add(info); - - if (!isTestnet) { + if (!isTestnet) + { hasValue.add(info.chainId); } - + else + { + testnetList.add(info.chainId); + } mapToTestNet.put(info.chainId, isTestnet); networkMap.put(info.chainId, info); } @@ -630,14 +695,20 @@ private void updateNetwork(NetworkInfo info, boolean isTestnet, long oldChainId) private void addNetwork(NetworkInfo info, boolean isTestnet) { list.add(info); - if (!isTestnet) { + if (!isTestnet) + { hasValue.add(info.chainId); } + else + { + testnetList.add(info.chainId); + } mapToTestNet.put(info.chainId, isTestnet); networkMap.put(info.chainId, info); } - public void remove(long chainId) { + public void remove(long chainId) + { removeNetwork(chainId); String networks = new Gson().toJson(this); @@ -686,24 +757,42 @@ private void addNetworks(List result, boolean withValue) { for (long networkId : hasValue) { - result.add(networkMap.get(networkId)); + if (!deprecatedNetworkList.contains(networkId)) + { + result.add(networkMap.get(networkId)); + } + } + + for (long networkId : hasValue) + { + if (deprecatedNetworkList.contains(networkId)) + { + result.add(networkMap.get(networkId)); + } } } else { - //sorted array - for (int i = 0; i < networkMap.size(); i++) + for (long networkId : testnetList) + { + if (!deprecatedNetworkList.contains(networkId)) + { + result.add(networkMap.get(networkId)); + } + } + + for (long networkId : testnetList) { - NetworkInfo info = networkMap.valueAt(i); - if (!hasValue.contains(info.chainId) && !result.contains(info)) + if (deprecatedNetworkList.contains(networkId)) { - result.add(info); + result.add(networkMap.get(networkId)); } } } } - public static String getChainOverrideAddress(long chainId) { + public static String getChainOverrideAddress(long chainId) + { return addressOverride.containsKey(chainId) ? addressOverride.get(chainId) : ""; } @@ -732,7 +821,8 @@ public NetworkInfo getNetworkByChain(long chainId) @Override public Single getLastTransactionNonce(Web3j web3j, String walletAddress) { - return Single.fromCallable(() -> { + return Single.fromCallable(() -> + { try { EthGetTransactionCount ethGetTransactionCount = web3j @@ -780,7 +870,7 @@ public List getSelectedFilters(boolean isMainNet) @Override public Long getDefaultNetwork(boolean isMainNet) { - return isMainNet ? CustomViewSettings.primaryChain : RINKEBY_ID; + return isMainNet ? CustomViewSettings.primaryChain : GOERLI_ID; } @Override @@ -835,7 +925,8 @@ public NetworkInfo[] getAllActiveNetworks() } @Override - public void addOnChangeDefaultNetwork(OnNetworkChangeListener onNetworkChanged) { + public void addOnChangeDefaultNetwork(OnNetworkChangeListener onNetworkChanged) + { onNetworkChangedListeners.add(onNetworkChanged); } @@ -852,8 +943,12 @@ public static List getAllMainNetworks() public static String getSecondaryNodeURL(long networkId) { NetworkInfo info = networkMap.get(networkId); - if (info != null) { return info.backupNodeUrl; } - else { + if (info != null) + { + return info.backupNodeUrl; + } + else + { return ""; } } @@ -903,16 +998,24 @@ public static BigInteger getMaxGasLimit(long chainId) public static String getNodeURLByNetworkId(long networkId) { NetworkInfo info = networkMap.get(networkId); - if (info != null) { return info.rpcServerUrl; } - else { return MAINNET_RPC_URL; } + if (info != null) + { + return info.rpcServerUrl; + } + else + { + return MAINNET_RPC_URL; + } } /** * This is used so as not to leak API credentials to web3; XInfuraAPI is the backup API key checked into github + * * @param networkId * @return */ - public static String getDefaultNodeURL(long networkId) { + public static String getDefaultNodeURL(long networkId) + { NetworkInfo info = networkMap.get(networkId); if (info != null) return info.rpcServerUrl; else return ""; @@ -920,9 +1023,12 @@ public static String getDefaultNodeURL(long networkId) { public static long getNetworkIdFromName(String name) { - if (!TextUtils.isEmpty(name)) { - for (int i = 0; i < networkMap.size(); i++) { - if (name.equals(networkMap.valueAt(i).name)) { + if (!TextUtils.isEmpty(name)) + { + for (int i = 0; i < networkMap.size(); i++) + { + if (name.equals(networkMap.valueAt(i).name)) + { return networkMap.valueAt(i).chainId; } } @@ -953,11 +1059,6 @@ public static List extraChains() return null; } - public static void addRequiredCredentials(long chainId, HttpService publicNodeService) - { - - } - public static List addDefaultNetworks() { return CustomViewSettings.alwaysVisibleChains; @@ -990,24 +1091,6 @@ public static int decimalOverride(String address, long chainId) return 0; } - public static String defaultDapp(long chainId) - { - String dapp = (chainId == MATIC_ID || chainId == MATIC_TEST_ID) ? POLYGON_HOMEPAGE : DEFAULT_HOMEPAGE; - return dapp; - } - - public static boolean isWithinHomePage(String url) - { - String homePageRoot = DEFAULT_HOMEPAGE.substring(0, DEFAULT_HOMEPAGE.length() - 1); //remove final slash - return (url != null && url.startsWith(homePageRoot)); - } - - public static boolean isDefaultDapp(String url) - { - return url != null && (url.equals(DEFAULT_HOMEPAGE) - || url.equals(POLYGON_HOMEPAGE)); - } - public Token getBlankOverrideToken(NetworkInfo networkInfo) { return createCurrencyToken(networkInfo); @@ -1015,7 +1098,8 @@ public Token getBlankOverrideToken(NetworkInfo networkInfo) public Single getBlankOverrideTokens(Wallet wallet) { - return Single.fromCallable(() -> { + return Single.fromCallable(() -> + { if (getBlankOverrideToken() == null) { return new Token[0]; @@ -1072,7 +1156,8 @@ public void setActiveMainnet(boolean isMainNet) preferences.setActiveMainnet(isMainNet); } - public void saveCustomRPCNetwork(String networkName, String rpcUrl, long chainId, String symbol, String blockExplorerUrl, String explorerApiUrl, boolean isTestnet, Long oldChainId) { + public void saveCustomRPCNetwork(String networkName, String rpcUrl, long chainId, String symbol, String blockExplorerUrl, String explorerApiUrl, boolean isTestnet, Long oldChainId) + { NetworkInfo builtInNetwork = builtinNetworkMap.get(chainId); boolean isCustom = builtInNetwork == null; @@ -1080,11 +1165,13 @@ public void saveCustomRPCNetwork(String networkName, String rpcUrl, long chainId customNetworks.save(info, isTestnet, oldChainId); } - public void removeCustomRPCNetwork(long chainId) { + public void removeCustomRPCNetwork(long chainId) + { customNetworks.remove(chainId); } - public static NetworkInfo getNetworkInfo(long chainId) { + public static NetworkInfo getNetworkInfo(long chainId) + { return networkMap.get(chainId); } @@ -1121,15 +1208,43 @@ public static String getChainSymbol(long chainId) } } + public static boolean isEventBlockLimitEnforced(long chainId) + { + if (chainId == POLYGON_ID || chainId == POLYGON_TEST_ID) + { + return true; + } + else + { + return false; + } + } + public static BigInteger getMaxEventFetch(long chainId) { - if (chainId == MATIC_ID || chainId == MATIC_TEST_ID) + if (chainId == POLYGON_ID || chainId == POLYGON_TEST_ID) { - return BigInteger.valueOf(3500L); + return BigInteger.valueOf(POLYGON_BLOCK_SEARCH_INTERVAL); } else { - return BigInteger.valueOf(10000L); + return BigInteger.valueOf(BLOCK_SEARCH_INTERVAL); + } + } + + public static String getNodeURLForEvents(long chainId) + { + if (chainId == POLYGON_ID) + { + return EthereumNetworkBase.FREE_POLYGON_RPC_URL; // Better than Infura for fetching events + } + else if (chainId == POLYGON_TEST_ID) + { + return EthereumNetworkBase.MUMBAI_FALLBACK_RPC_URL; + } + else + { + return getNodeURLByNetworkId(chainId); } } @@ -1138,4 +1253,15 @@ public NetworkInfo getBuiltInNetwork(long chainId) { return builtinNetworkMap.get(chainId); } + + public static boolean isNetworkDeprecated(long chainId) + { + return deprecatedNetworkList.contains(chainId); + } + + @Override + public void commitPrefs() + { + preferences.commit(); + } } diff --git a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepository.java b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepository.java index 8d219bcf42..2c2be94338 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepository.java @@ -12,13 +12,11 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.GNOSIS_ID; public class EthereumNetworkRepository extends EthereumNetworkBase { @@ -68,11 +66,11 @@ private void buildPopularTokenMap(List networkFilters) popularTokens.put(unknownToken.address.toLowerCase(), new ContractLocator(unknownToken.address, MAINNET_ID)); } } - if (networkFilters == null || networkFilters.contains(XDAI_ID)) + if (networkFilters == null || networkFilters.contains(GNOSIS_ID)) { for (UnknownToken unknownToken: knownContract.getXDAI()) { - popularTokens.put(unknownToken.address.toLowerCase(), new ContractLocator(unknownToken.address, XDAI_ID)); + popularTokens.put(unknownToken.address.toLowerCase(), new ContractLocator(unknownToken.address, GNOSIS_ID)); } } } diff --git a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepositoryType.java index fb318ada50..86ec54803d 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepositoryType.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkRepositoryType.java @@ -5,7 +5,6 @@ import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.repository.entity.RealmToken; import org.web3j.protocol.Web3j; @@ -54,6 +53,7 @@ public interface EthereumNetworkRepositoryType { void setHasSetNetworkFilters(); boolean isMainNetSelected(); void setActiveMainnet(boolean isMainNet); + String getDappBrowserRPC(long chainId); void saveCustomRPCNetwork(String networkName, String rpcUrl, long chainId, String symbol, String blockExplorerUrl, String explorerApiUrl, boolean isTestnet, Long oldChainId); void removeCustomRPCNetwork(long chainId); @@ -62,4 +62,6 @@ public interface EthereumNetworkRepositoryType { boolean hasLockedGas(long chainId); NetworkInfo getBuiltInNetwork(long chainId); + + void commitPrefs(); } diff --git a/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java b/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java new file mode 100644 index 0000000000..7e32cd0d84 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java @@ -0,0 +1,41 @@ +package com.alphawallet.app.repository; + +import static com.alphawallet.app.repository.EthereumNetworkBase.INFURA_DOMAIN; +import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_BAOBAB_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_ID; + +import android.text.TextUtils; + +import org.web3j.protocol.http.HttpService; + +import okhttp3.Request; + +public class HttpServiceHelper +{ + public static void addRequiredCredentials(long chainId, HttpService httpService, String klaytnKey, String infuraKey, boolean usesProductionKey) + { + String serviceUrl = httpService.getUrl(); + if ((chainId == KLAYTN_BAOBAB_ID || chainId == KLAYTN_ID) && usesProductionKey) + { + httpService.addHeader("x-chain-id", Long.toString(chainId)); + httpService.addHeader("Authorization", "Basic " + klaytnKey); + } + else if (serviceUrl != null && usesProductionKey && serviceUrl.contains(INFURA_DOMAIN) && !TextUtils.isEmpty(infuraKey)) + { + httpService.addHeader("Authorization", "Basic " + infuraKey); + } + } + + public static void addRequiredCredentials(long chainId, Request.Builder service, String klaytnKey, String infuraKey, boolean usesProductionKey, boolean isInfura) + { + if ((chainId == KLAYTN_BAOBAB_ID || chainId == KLAYTN_ID) && usesProductionKey) + { + service.addHeader("x-chain-id", Long.toString(chainId)); + service.addHeader("Authorization", "Basic " + klaytnKey); + } + else if (isInfura && usesProductionKey && !TextUtils.isEmpty(infuraKey)) + { + service.addHeader("Authorization", "Basic " + infuraKey); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java b/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java new file mode 100644 index 0000000000..8642e9bb92 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java @@ -0,0 +1,38 @@ +package com.alphawallet.app.repository; + +public interface KeyProvider +{ + String getBSCExplorerKey(); + + String getAnalyticsKey(); + + String getEtherscanKey(); + + String getPolygonScanKey(); + + String getAuroraScanKey(); + + String getCovalentKey(); + + String getKlaytnKey(); + + String getInfuraKey(); + + String getSecondaryInfuraKey(); + + String getTertiaryInfuraKey(); + + String getRampKey(); + + String getOpenSeaKey(); + + String getMailchimpKey(); + + String getCoinbasePayAppId(); + + String getWalletConnectProjectId(); + + String getInfuraSecret(); + + String getUnstoppableDomainsKey(); +} diff --git a/app/src/main/java/com/alphawallet/app/repository/KeyProviderFactory.java b/app/src/main/java/com/alphawallet/app/repository/KeyProviderFactory.java new file mode 100644 index 0000000000..617766e29c --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/KeyProviderFactory.java @@ -0,0 +1,8 @@ +package com.alphawallet.app.repository; + +public class KeyProviderFactory +{ + public static KeyProvider get() { + return new KeyProviderJNIImpl(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java b/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java new file mode 100644 index 0000000000..26684bcb1f --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java @@ -0,0 +1,43 @@ +package com.alphawallet.app.repository; + +public class KeyProviderJNIImpl implements KeyProvider +{ + public KeyProviderJNIImpl() + { + System.loadLibrary("keys"); + } + + public native String getInfuraKey(); + + public native String getSecondaryInfuraKey(); + + public native String getTertiaryInfuraKey(); + + public native String getBSCExplorerKey(); + + public native String getAnalyticsKey(); + + public native String getEtherscanKey(); + + public native String getPolygonScanKey(); + + public native String getAuroraScanKey(); + + public native String getCovalentKey(); + + public native String getKlaytnKey(); + + public native String getRampKey(); + + public native String getOpenSeaKey(); + + public native String getMailchimpKey(); + + public native String getCoinbasePayAppId(); + + public native String getWalletConnectProjectId(); + + public native String getInfuraSecret(); + + public native String getUnstoppableDomainsKey(); +} diff --git a/app/src/main/java/com/alphawallet/app/repository/LocaleRepository.java b/app/src/main/java/com/alphawallet/app/repository/LocaleRepository.java index bb32a89689..923cd4e7f9 100644 --- a/app/src/main/java/com/alphawallet/app/repository/LocaleRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/LocaleRepository.java @@ -18,7 +18,8 @@ public class LocaleRepository implements LocaleRepositoryType { "es", "fr", "vi", - "my" + "my", + "id" }; private final PreferenceRepositoryType preferences; diff --git a/app/src/main/java/com/alphawallet/app/repository/OnRampRepository.java b/app/src/main/java/com/alphawallet/app/repository/OnRampRepository.java index bcd144be34..4baa40aa22 100644 --- a/app/src/main/java/com/alphawallet/app/repository/OnRampRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/OnRampRepository.java @@ -4,53 +4,43 @@ import android.net.Uri; import com.alphawallet.app.C; -import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.OnRampContract; import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.util.Utils; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.util.Map; -public class OnRampRepository implements OnRampRepositoryType { +public class OnRampRepository implements OnRampRepositoryType +{ public static final String DEFAULT_PROVIDER = "Ramp"; private static final String RAMP = "ramp"; private static final String ONRAMP_CONTRACTS_FILE_NAME = "onramp_contracts.json"; - static - { - System.loadLibrary("keys"); - } - private final Context context; - private final AnalyticsServiceType analyticsService; + private final KeyProvider keyProvider = KeyProviderFactory.get(); - public OnRampRepository(Context context, AnalyticsServiceType analyticsService) + public OnRampRepository(Context context) { this.context = context; - this.analyticsService = analyticsService; } - public static native String getRampKey(); - @Override public String getUri(String address, Token token) { - if (token != null) { + if (token != null) + { OnRampContract contract = getContract(token); - - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(contract.getSymbol()); - analyticsService.track(C.AN_USE_ONRAMP, analyticsProperties); - - switch (contract.getProvider().toLowerCase()) { + switch (contract.getProvider().toLowerCase()) + { case RAMP: default: return buildRampUri(address, contract.getSymbol()).toString(); } - } else { + } + else + { return buildRampUri(address, "").toString(); } } @@ -71,7 +61,8 @@ public OnRampContract getContract(Token token) private Map getKnownContracts() { return new Gson().fromJson(Utils.loadJSONFromAsset(context, ONRAMP_CONTRACTS_FILE_NAME), - new TypeToken>() { + new TypeToken>() + { }.getType()); } @@ -80,7 +71,7 @@ private Uri buildRampUri(String address, String symbol) Uri.Builder builder = new Uri.Builder(); builder.scheme("https") .authority("buy.ramp.network") - .appendQueryParameter("hostApiKey", getRampKey()) + .appendQueryParameter("hostApiKey", keyProvider.getRampKey()) .appendQueryParameter("hostLogoUrl", C.ALPHAWALLET_LOGO_URI) .appendQueryParameter("hostAppName", "AlphaWallet") .appendQueryParameter("userAddress", address); diff --git a/app/src/main/java/com/alphawallet/app/repository/PreferenceRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/PreferenceRepositoryType.java index cd1bdfe879..afe6fc1a05 100644 --- a/app/src/main/java/com/alphawallet/app/repository/PreferenceRepositoryType.java +++ b/app/src/main/java/com/alphawallet/app/repository/PreferenceRepositoryType.java @@ -2,7 +2,10 @@ import com.alphawallet.app.entity.CurrencyItem; -public interface PreferenceRepositoryType { +import java.util.Set; + +public interface PreferenceRepositoryType +{ String getCurrentWalletAddress(); void setCurrentWalletAddress(String address); @@ -43,9 +46,10 @@ public interface PreferenceRepositoryType { void setFullScreenState(boolean state); - void setUse1559Transactions(boolean toggleState); boolean getUse1559Transactions(); + void setUse1559Transactions(boolean toggleState); + boolean isActiveMainnet(); void setActiveMainnet(boolean state); @@ -54,46 +58,73 @@ public interface PreferenceRepositoryType { void setShownTestNetWarning(); + String getPriceAlerts(); + void setPriceAlerts(String json); - String getPriceAlerts(); void setHasSetNetworkFilters(); + boolean hasSetNetworkFilters(); + void blankHasSetNetworkFilters(); void commit(); void incrementLaunchCount(); + int getLaunchCount(); + void resetLaunchCount(); void setRateAppShown(); + boolean getRateAppShown(); void setShowZeroBalanceTokens(boolean shouldShow); + boolean shouldShowZeroBalanceTokens(); int getUpdateWarningCount(); - void setUpdateWarningCount(int count); - int getUpdateAsksCount(); - void setUpdateAsksCount(int count); + void setUpdateWarningCount(int count); long getInstallTime(); + void setInstallTime(long time); String getUniqueId(); + void setUniqueId(String uuid); boolean isMarshMallowWarningShown(); + void setMarshMallowWarning(boolean shown); void storeLastFragmentPage(int ordinal); + int getLastFragmentPage(); int getLastVersionCode(int currentCode); + void setLastVersionCode(int code); int getTheme(); + void setTheme(int state); + + boolean isNewWallet(String address); + + void setNewWallet(String address, boolean isNewWallet); + + Set getSelectedSwapProviders(); + + void setSelectedSwapProviders(Set swapProviders); + + boolean isAnalyticsEnabled(); + + void setAnalyticsEnabled(boolean isEnabled); + + boolean isCrashReportingEnabled(); + + void setCrashReportingEnabled(boolean isEnabled); } diff --git a/app/src/main/java/com/alphawallet/app/repository/SharedPreferenceRepository.java b/app/src/main/java/com/alphawallet/app/repository/SharedPreferenceRepository.java index 08ff7af6a0..3e824ee507 100644 --- a/app/src/main/java/com/alphawallet/app/repository/SharedPreferenceRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/SharedPreferenceRepository.java @@ -4,13 +4,15 @@ import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import com.alphawallet.app.C; import com.alphawallet.app.entity.CurrencyItem; -import com.alphawallet.app.entity.WalletPage; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; public class SharedPreferenceRepository implements PreferenceRepositoryType { private static final String CURRENT_ACCOUNT_ADDRESS_KEY = "current_account_address"; @@ -35,16 +37,19 @@ public class SharedPreferenceRepository implements PreferenceRepositoryType { private static final String SET_NETWORK_FILTERS = "set_filters"; private static final String SHOULD_SHOW_ROOT_WARNING = "should_show_root_warning"; private static final String UPDATE_WARNINGS = "update_warns"; - private static final String UPDATE_ASKS = "update_asks"; private static final String INSTALL_TIME = "install_time"; public static final String DEVICE_LOCALE = "device_locale"; public static final String DEVICE_COUNTRY = "device_country"; public static final String MARSHMALLOW_SUPPORT_WARNING = "marshmallow_version_support_warning_shown"; private static final String LAST_FRAGMENT_ID = "lastfrag_id"; private static final String LAST_VERSION_CODE = "last_version_code"; + private static final String SELECTED_SWAP_PROVIDERS_KEY = "selected_exchanges"; + private static final String ANALYTICS_KEY = "analytics_key"; + private static final String CRASH_REPORTING_KEY = "crash_reporting_key"; private static final String RATE_APP_SHOWN = "rate_us_shown"; private static final String LAUNCH_COUNT = "launch_count"; + private static final String NEW_WALLET = "new_wallet_"; private final SharedPreferences pref; @@ -297,16 +302,6 @@ public void setUpdateWarningCount(int count) { pref.edit().putInt(UPDATE_WARNINGS, count).apply(); } - @Override - public int getUpdateAsksCount() { - return pref.getInt(UPDATE_ASKS, 0); - } - - @Override - public void setUpdateAsksCount(int count) { - pref.edit().putInt(UPDATE_ASKS, count).apply(); - } - @Override public void setInstallTime(long time) { pref.edit().putLong(INSTALL_TIME, time).apply(); @@ -376,4 +371,58 @@ public void setTheme(int state) { pref.edit().putInt(THEME_KEY, state).apply(); } + + @Override + public boolean isNewWallet(String address) + { + return pref.getBoolean(keyOf(address), false); + } + + @Override + public void setNewWallet(String address, boolean isNewWallet) + { + pref.edit().putBoolean(keyOf(address), isNewWallet).apply(); + } + + @Override + public Set getSelectedSwapProviders() + { + return pref.getStringSet(SELECTED_SWAP_PROVIDERS_KEY, new HashSet<>()); + } + + @Override + public void setSelectedSwapProviders(Set swapProviders) + { + pref.edit().putStringSet(SELECTED_SWAP_PROVIDERS_KEY, swapProviders).apply(); + } + + @Override + public boolean isAnalyticsEnabled() + { + return pref.getBoolean(ANALYTICS_KEY, true); + } + + @Override + public void setAnalyticsEnabled(boolean isEnabled) + { + pref.edit().putBoolean(ANALYTICS_KEY, isEnabled).apply(); + } + + @Override + public boolean isCrashReportingEnabled() + { + return pref.getBoolean(CRASH_REPORTING_KEY, true); + } + + @Override + public void setCrashReportingEnabled(boolean isEnabled) + { + pref.edit().putBoolean(CRASH_REPORTING_KEY, isEnabled).apply(); + } + + @NonNull + private String keyOf(String address) + { + return NEW_WALLET + address.toLowerCase(Locale.ENGLISH); + } } diff --git a/app/src/main/java/com/alphawallet/app/repository/SwapRepository.java b/app/src/main/java/com/alphawallet/app/repository/SwapRepository.java new file mode 100644 index 0000000000..155aa473fb --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/SwapRepository.java @@ -0,0 +1,36 @@ +package com.alphawallet.app.repository; + +import android.content.Context; + +import com.alphawallet.app.entity.lifi.SwapProvider; +import com.alphawallet.app.util.Utils; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.util.List; + +public class SwapRepository implements SwapRepositoryType +{ + public static final String FETCH_CHAINS = "https://li.quest/v1/chains"; + public static final String FETCH_TOKENS = "https://li.quest/v1/connections"; + public static final String FETCH_QUOTE = "https://li.quest/v1/quote"; + public static final String FETCH_TOOLS = "https://li.quest/v1/tools"; + public static final String FETCH_ROUTES = "https://li.quest/v1/advanced/routes"; + private static final String SWAP_PROVIDERS_FILENAME = "swap_providers_list.json"; + + private final Context context; + + public SwapRepository(Context context) + { + this.context = context; + } + + @Override + public List getProviders() + { + return new Gson().fromJson(Utils.loadJSONFromAsset(context, SWAP_PROVIDERS_FILENAME), + new TypeToken>() + { + }.getType()); + } +} diff --git a/app/src/main/java/com/alphawallet/app/repository/SwapRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/SwapRepositoryType.java new file mode 100644 index 0000000000..2821fc08a2 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/SwapRepositoryType.java @@ -0,0 +1,10 @@ +package com.alphawallet.app.repository; + +import com.alphawallet.app.entity.lifi.SwapProvider; + +import java.util.List; + +public interface SwapRepositoryType +{ + List getProviders(); +} diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java b/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java index eff567fa1e..4850a2b0d9 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java @@ -20,49 +20,66 @@ import io.reactivex.Single; import io.realm.Realm; -public interface TokenLocalSource { +public interface TokenLocalSource +{ Single saveToken(Wallet wallet, Token token); + Single saveTokens(Wallet wallet, Token[] items); + boolean updateTokenBalance(Wallet wallet, Token token, BigDecimal balance, List balanceArray); + Token fetchToken(long chainId, Wallet wallet, String address); + void setEnable(Wallet wallet, Token token, boolean isEnabled); + String getTokenImageUrl(long chainId, String address); - void deleteRealmToken(long chainId, Wallet wallet, String address); + + void deleteRealmTokens(Wallet wallet, List tcmList); + void storeTokenUrl(long chainId, String address, String imageUrl); - Token[] initNFTAssets(Wallet wallet, Token[] tokens); + + Token initNFTAssets(Wallet wallet, Token tokens); Single fetchTokenMetas(Wallet wallet, List networkFilters, AssetDefinitionService svs); Single fetchAllTokenMetas(Wallet wallet, List networkFilters, - String seachTerm); + String seachTerm); TokenCardMeta[] fetchTokenMetasForUpdate(Wallet wallet, List networkFilters); Single fetchAllTokensWithNameIssue(String walletAddress, List networkFilters); + Single fetchAllTokensWithBlankName(String walletAddress, List networkFilters); Single fixFullNames(Wallet wallet, AssetDefinitionService svs); void updateEthTickers(Map ethTickers); + void updateERC20Tickers(long chainId, Map erc20Tickers); + void removeOutdatedTickers(); Realm getRealmInstance(Wallet wallet); + Realm getTickerRealmInstance(); TokenTicker getCurrentTicker(Token token); + TokenTicker getCurrentTicker(String key); void setVisibilityChanged(Wallet wallet, Token token); boolean hasVisibilityBeenChanged(Token token); + boolean getEnabled(Token token); void updateNFTAssets(String wallet, Token erc721Token, List additions, List removals); + void storeAsset(String wallet, Token token, BigInteger tokenId, NFTAsset asset); int storeTokensMapping(Pair, Map> mappings); + long getLastMappingsUpdate(); Single> getTotalValue(String currentAddress, List networkFilters); @@ -74,6 +91,7 @@ Single fetchAllTokenMetas(Wallet wallet, List networkFilt Single> getTickerUpdateList(List networkFilter); ContractAddress getBaseToken(long chainId, String address); + TokenGroup getTokenGroup(long chainId, String address, ContractType type); void updateTicker(long chainId, String address, TokenTicker ticker); diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java index eaf432cb4d..f72663b0e5 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java @@ -1,6 +1,5 @@ package com.alphawallet.app.repository; -import static com.alphawallet.app.entity.tokenscript.TokenscriptFunction.ZERO_ADDRESS; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; @@ -22,14 +21,15 @@ import com.alphawallet.app.entity.tokendata.TokenTicker; import com.alphawallet.app.entity.tokens.ERC721Ticket; import com.alphawallet.app.entity.tokens.ERC721Token; +import com.alphawallet.app.entity.tokens.Ticket; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.entity.tokens.TokenInfo; import com.alphawallet.app.service.AWHttpService; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TickerService; -import com.alphawallet.app.util.AWEnsResolver; import com.alphawallet.app.util.Utils; +import com.alphawallet.app.util.ens.AWEnsResolver; import com.alphawallet.token.entity.ContractAddress; import com.alphawallet.token.entity.MagicLinkData; @@ -92,12 +92,14 @@ public class TokenRepository implements TokenRepositoryType { public static final BigInteger INTERFACE_BALANCES_721_TICKET = new BigInteger ("c84aae17", 16); public static final BigInteger INTERFACE_SUPERRARE = new BigInteger ("5b5e139f", 16); public static final BigInteger INTERFACE_ERC1155 = new BigInteger("d9b67a26", 16); + public static final BigInteger INTERFACE_ERC721_ENUMERABLE = new BigInteger("780e9d63", 16); private static final int NODE_COMMS_ERROR = -1; private static final int CONTRACT_BALANCE_NULL = -2; private final Map web3jNodeServers; private AWEnsResolver ensResolver; + private String currentAddress; public TokenRepository( EthereumNetworkRepositoryType ethereumNetworkRepository, @@ -113,12 +115,14 @@ public TokenRepository( this.tickerService = tickerService; web3jNodeServers = new ConcurrentHashMap<>(); + currentAddress = ethereumNetworkRepository.getCurrentWalletAddress(); } private void buildWeb3jClient(NetworkInfo networkInfo) { AWHttpService publicNodeService = new AWHttpService(networkInfo.rpcServerUrl, networkInfo.backupNodeUrl, okClient, false); - EthereumNetworkRepository.addRequiredCredentials(networkInfo.chainId, publicNodeService); + HttpServiceHelper.addRequiredCredentials(networkInfo.chainId, publicNodeService, KeyProviderFactory.get().getKlaytnKey(), + KeyProviderFactory.get().getInfuraSecret(), EthereumNetworkBase.usesProductionKey); web3jNodeServers.put(networkInfo.chainId, Web3j.build(publicNodeService)); } @@ -132,14 +136,12 @@ private Web3j getService(long chainId) } @Override - public Single checkInterface(Token[] tokens, Wallet wallet) + public Single checkInterface(Token token, Wallet wallet) { return Single.fromCallable(() -> { //check if the token interface has been checked - for (int i = 0; i < tokens.length; i++) - { - Token t = tokens[i]; - if (t.getInterfaceSpec() == ContractType.ERC721_UNDETERMINED || t.getInterfaceSpec() == ContractType.MAYBE_ERC20 || !t.checkBalanceType()) //balance type appears to be wrong + Token t = token; + if (t.getInterfaceSpec() == ContractType.ERC721_UNDETERMINED || t.getInterfaceSpec() == ContractType.MAYBE_ERC20) //balance type appears to be wrong { ContractType type = determineCommonType(t.tokenInfo) .onErrorReturnItem(t.getInterfaceSpec()).blockingGet(); @@ -153,8 +155,6 @@ public Single checkInterface(Token[] tokens, Wallet wallet) type = ContractType.ERC20; break; } - //couldn't determine the type, try again next time - continue; case ERC20: if (t.getInterfaceSpec() != ContractType.MAYBE_ERC20) { @@ -164,33 +164,36 @@ public Single checkInterface(Token[] tokens, Wallet wallet) case ERC1155: break; case ERC721: + case ERC721_ENUMERABLE: case ERC721_LEGACY: Map NFTBalance = t.getTokenAssets(); //add balance from Opensea t.balance = checkUint256Balance(wallet, tInfo.chainId, tInfo.address); //get balance for wallet from contract if (TextUtils.isEmpty(tInfo.name + tInfo.symbol)) tInfo = new TokenInfo(tInfo.address, " ", " ", tInfo.decimals, tInfo.isEnabled, tInfo.chainId); //ensure we don't keep overwriting this t = new ERC721Token(tInfo, NFTBalance, t.balance, System.currentTimeMillis(), t.getNetworkName(), type); - t.lastTxTime = tokens[i].lastTxTime; - t.setTokenWallet(wallet.address); - tokens[i] = t; + t.lastTxTime = token.lastTxTime; break; case ERC721_TICKET: List balanceFromOpenSea = t.getArrayBalance(); t = new ERC721Ticket(t.tokenInfo, balanceFromOpenSea, System.currentTimeMillis(), t.getNetworkName(), ContractType.ERC721_TICKET); - t.setTokenWallet(wallet.address); - tokens[i] = t; break; default: type = ContractType.ERC721; } t.setInterfaceSpec(type); + t.setTokenWallet(wallet.address); } - } - return tokens; + return t; }).flatMap(this::checkTokenData); } + @Override + public void updateLocalAddress(String walletAddress) + { + currentAddress = walletAddress; + } + @Override public TokenCardMeta[] fetchTokenMetasForUpdate(Wallet wallet, List networkFilters) { @@ -345,7 +348,7 @@ public void storeAsset(String wallet, Token token, BigInteger tokenId, NFTAsset } @Override - public Token[] initNFTAssets(Wallet wallet, Token[] token) + public Token initNFTAssets(Wallet wallet, Token token) { return localSource.initNFTAssets(wallet, token); } @@ -354,7 +357,7 @@ public Token[] initNFTAssets(Wallet wallet, Token[] token) public Single resolveENS(long chainId, String ensName) { if (ensResolver == null) ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), context); - return ensResolver.resolveENSAddress(ensName, true); + return ensResolver.resolveENSAddress(ensName); } @Override @@ -370,9 +373,22 @@ public void setVisibilityChanged(Wallet wallet, Token token) } @Override - public Single update(String contractAddr, long chainId) + public Single update(String contractAddr, long chainId, ContractType type) { - return setupTokensFromLocal(contractAddr, chainId); + switch (type) + { + case ERC721: + case ERC721_ENUMERABLE: + case ERC875_LEGACY: + case ERC721_LEGACY: + case ERC721_UNDETERMINED: + case ERC1155: + case ERC875: + case ERC721_TICKET: + return setupNFTFromLocal(contractAddr, chainId); + default: + return setupTokensFromLocal(contractAddr, chainId); + } } @Override @@ -387,6 +403,8 @@ public String getTokenImageUrl(long networkId, String address) return localSource.getTokenImageUrl(networkId, address); } + //TODO: Refactor this so the balance update is abstracted into the Token itself + // Once the token is updated we can store it. May need to make the token internal balance non-final private Single updateBalance(final Wallet wallet, final Token token) { return Single.fromCallable(() -> { @@ -405,10 +423,12 @@ private Single updateBalance(final Wallet wallet, final Token token) case ERC875: case ERC875_LEGACY: balanceArray = getBalanceArray875(wallet, token.tokenInfo.chainId, token.getAddress()); + thisToken = new Ticket(thisToken, balanceArray); balance = BigDecimal.valueOf(balanceArray.size()); break; case ERC721_LEGACY: case ERC721: + case ERC721_ENUMERABLE: balance = updateERC721Balance(token, wallet); break; case ERC20: @@ -436,7 +456,12 @@ private Single updateBalance(final Wallet wallet, final Token token) break; } - if (!balance.equals(BigDecimal.valueOf(-1)) || balanceArray != null) + if (balance.equals(BigDecimal.valueOf(-2))) + { + //token may have been self-destructed. Check name/symbol for 0x + checkDestroyedToken(wallet, token); + } + else if (!balance.equals(BigDecimal.valueOf(-1)) || balanceArray != null) { localSource.updateTokenBalance(wallet, thisToken, balance, balanceArray); } @@ -454,15 +479,45 @@ private Single updateBalance(final Wallet wallet, final Token token) }); } + private void checkDestroyedToken(Wallet wallet, Token token) + { + try + { + NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(token.tokenInfo.chainId); + String responseValue = callSmartContractFunction(nameOf(), token.tokenInfo.address, network, wallet); + + if (responseValue.equals("0x")) + { + responseValue = callSmartContractFunction(symbolOf(), token.tokenInfo.address, network, wallet); + if (responseValue.equals("0x")) + { + //mark token destroyed + localSource.updateTokenBalance(wallet, token, BigDecimal.valueOf(-2), null); + } + } + } + catch (Exception e) + { + // take no action unless it's a confirmed destruction + } + } + + @Override + public void deleteRealmTokens(Wallet wallet, List tcmList) + { + localSource.deleteRealmTokens(wallet, tcmList); + } + private BigDecimal updateERC721Balance(Token token, Wallet wallet) { token.setTokenWallet(wallet.address); + token.balance = checkUint256Balance(wallet, token.tokenInfo.chainId, token.getAddress()); try (Realm realm = getRealmInstance(wallet)) { token.updateBalance(realm); } - return checkUint256Balance(wallet, token.tokenInfo.chainId, token.getAddress()); + return token.balance; } private BigDecimal updateERC1155Balance(Token token, Wallet wallet) @@ -476,13 +531,14 @@ private BigDecimal updateERC1155Balance(Token token, Wallet wallet) return newBalance; } + //Batch Balance private Single updateBalances(Wallet wallet, Token[] tokens) { return Single.fromCallable(() -> { for (Token t : tokens) { //get balance of any token here - if (t.isERC721() || t.isERC20()) t.balance = checkUint256Balance(wallet, t.tokenInfo.chainId, t.getAddress()); + if (t.isERC20() || t.isNonFungible()) t.balance = checkUint256Balance(wallet, t.tokenInfo.chainId, t.getAddress()); } return tokens; }); @@ -500,6 +556,7 @@ private BigDecimal checkUint256Balance(@NonNull Wallet wallet, long chainId, Str if (!TextUtils.isEmpty(responseValue)) { + if (responseValue.equals("0x")) return BigDecimal.valueOf(-2); List response = FunctionReturnDecoder.decode(responseValue, function.getOutputParameters()); if (response.size() > 0) balance = new BigDecimal(((Uint256) response.get(0)).getValue()); } @@ -530,6 +587,7 @@ private Token wrappedCheckUint256Balance(@NonNull Wallet wallet, @NonNull TokenI NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); String responseValue = callSmartContractFunction(function, tokenInfo.address, network, wallet); + if (!TextUtils.isEmpty(responseValue)) { List response = FunctionReturnDecoder.decode(responseValue, function.getOutputParameters()); @@ -542,8 +600,7 @@ private Token wrappedCheckUint256Balance(@NonNull Wallet wallet, @NonNull TokenI List testBalance = getBalanceArray721Ticket(wallet, tokenInfo); if (testBalance.size() > 0) { - Token[] tkr = checkInterface(new Token[]{token}, wallet).onErrorReturnItem(new Token[]{token}).blockingGet(); - token = tkr.length == 1 ? tkr[0] : token; + token = checkInterface(token, wallet).onErrorReturnItem(token).blockingGet(); if (token.getInterfaceSpec() == ContractType.ERC721_TICKET) { for (BigInteger tokenId : testBalance) { token.addAssetToTokenBalanceAssets(tokenId, null); } @@ -553,8 +610,7 @@ private Token wrappedCheckUint256Balance(@NonNull Wallet wallet, @NonNull TokenI else if (balance.equals(BigDecimal.valueOf(32)) && responseValue.length() > 66) { //this is a token returning an array balance. Test the interface and update - Token[] tkr = checkInterface(new Token[]{token}, wallet).onErrorReturnItem(new Token[]{token}).blockingGet(); - token = tkr.length == 1 ? tkr[0] : token; + token = checkInterface(token, wallet).onErrorReturnItem(token).blockingGet(); } } } @@ -659,7 +715,7 @@ public Observable burnListenerObservable(String contr private T getContractData(NetworkInfo network, String address, Function function, T type) throws Exception { - String responseValue = callSmartContractFunction(function, address, network, new Wallet(ZERO_ADDRESS)); + String responseValue = callSmartContractFunction(function, address, network, new Wallet(currentAddress)); if (TextUtils.isEmpty(responseValue)) { @@ -788,7 +844,7 @@ private String filterAscii(String name) private int getDecimals(String address, NetworkInfo network) throws Exception { if (EthereumNetworkRepository.decimalOverride(address, network.chainId) > 0) return EthereumNetworkRepository.decimalOverride(address, network.chainId); Function function = decimalsOf(); - String responseValue = callSmartContractFunction(function, address, network, new Wallet(ZERO_ADDRESS)); + String responseValue = callSmartContractFunction(function, address, network, new Wallet(currentAddress)); if (TextUtils.isEmpty(responseValue)) return 18; List response = FunctionReturnDecoder.decode( @@ -1050,17 +1106,23 @@ private Single setupTokensFromLocal(String address, long chainId) }).onErrorReturnItem(new TokenInfo()); } - private Single checkTokenData(Token[] tokens) + private Single setupNFTFromLocal(String address, long chainId) { return Single.fromCallable(() -> { - for (int i = 0; i < tokens.length; i++) - { - tokens[i] = updateTokenNameIfRequired(tokens[i]) - .onErrorReturnItem(tokens[i]).blockingGet(); - } + NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(chainId); + return new TokenInfo( + address, + getContractData(network, address, nameOf(), ""), + getContractData(network, address, symbolOf(), ""), + 0, + false, chainId); + }).onErrorReturnItem(new TokenInfo()); + } - return tokens; - }); + private Single checkTokenData(Token token) + { + return updateTokenNameIfRequired(token) + .onErrorReturnItem(token); } private Single updateTokenNameIfRequired(final Token t) @@ -1105,6 +1167,8 @@ public Single determineCommonType(TokenInfo tokenInfo) { if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_BALANCES_721_TICKET), Boolean.TRUE)) returnType = ContractType.ERC721_TICKET; + else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_ERC721_ENUMERABLE), Boolean.TRUE)) + returnType = ContractType.ERC721_ENUMERABLE; else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_OFFICIAL_ERC721), Boolean.TRUE)) returnType = ContractType.ERC721; else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_SUPERRARE), Boolean.TRUE)) @@ -1135,7 +1199,7 @@ else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE catch (Exception e) { isERC875 = false; } try { - responseValue = callSmartContractFunction(balanceOf(ZERO_ADDRESS), tokenInfo.address, network, new Wallet(ZERO_ADDRESS)); + responseValue = callSmartContractFunction(balanceOf(currentAddress), tokenInfo.address, network, new Wallet(currentAddress)); } catch (Exception e) { responseValue = ""; } @@ -1182,6 +1246,28 @@ public void addImageUrl(long networkId, String address, String imageUrl) localSource.storeTokenUrl(networkId, address, imageUrl); } + public static Web3j getWeb3jServiceForEvents(long chainId) + { + OkHttpClient okClient = new OkHttpClient.Builder() + .connectTimeout(C.CONNECT_TIMEOUT * 3, TimeUnit.SECONDS) //events can take longer to render + .connectTimeout(C.READ_TIMEOUT * 3, TimeUnit.SECONDS) + .writeTimeout(C.LONG_WRITE_TIMEOUT, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + + String nodeUrl = EthereumNetworkBase.getNodeURLForEvents(chainId); + String secondaryNode = EthereumNetworkRepository.getSecondaryNodeURL(chainId); + if (nodeUrl.equals(secondaryNode)) //ensure backup node is different + { + secondaryNode = EthereumNetworkRepository.getNodeURLByNetworkId(chainId); + } + + AWHttpService publicNodeService = new AWHttpService(nodeUrl, secondaryNode, okClient, false); + HttpServiceHelper.addRequiredCredentials(chainId, publicNodeService, KeyProviderFactory.get().getKlaytnKey(), + KeyProviderFactory.get().getInfuraSecret(), EthereumNetworkBase.usesProductionKey); + return Web3j.build(publicNodeService); + } + public static Web3j getWeb3jService(long chainId) { OkHttpClient okClient = new OkHttpClient.Builder() @@ -1190,8 +1276,10 @@ public static Web3j getWeb3jService(long chainId) .writeTimeout(C.LONG_WRITE_TIMEOUT, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); - AWHttpService publicNodeService = new AWHttpService(EthereumNetworkRepository.getNodeURLByNetworkId (chainId), EthereumNetworkRepository.getSecondaryNodeURL(chainId), okClient, false); - EthereumNetworkRepository.addRequiredCredentials(chainId, publicNodeService); + + AWHttpService publicNodeService = new AWHttpService(EthereumNetworkRepository.getNodeURLByNetworkId(chainId), EthereumNetworkRepository.getSecondaryNodeURL(chainId), okClient, false); + HttpServiceHelper.addRequiredCredentials(chainId, publicNodeService, KeyProviderFactory.get().getKlaytnKey(), + KeyProviderFactory.get().getInfuraSecret(), EthereumNetworkBase.usesProductionKey); return Web3j.build(publicNodeService); } diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java index 164053bc2c..11c5c946ad 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java @@ -4,21 +4,11 @@ import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.ContractType; -import com.alphawallet.app.entity.SubscribeWrapper; -import com.alphawallet.app.entity.tokendata.TokenGroup; -import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.entity.tokens.TokenCardMeta; -import com.alphawallet.app.entity.tokens.TokenInfo; -import com.alphawallet.app.entity.tokendata.TokenTicker; -import com.alphawallet.app.entity.TransferFromEventResponse; -import com.alphawallet.app.entity.Wallet; -import com.alphawallet.app.service.AssetDefinitionService; - -import io.reactivex.disposables.Disposable; - import com.alphawallet.app.entity.TransferFromEventResponse; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.nftassets.NFTAsset; +import com.alphawallet.app.entity.tokendata.TokenGroup; +import com.alphawallet.app.entity.tokendata.TokenTicker; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.entity.tokens.TokenInfo; @@ -33,27 +23,43 @@ import io.reactivex.Single; import io.realm.Realm; -public interface TokenRepositoryType { - +public interface TokenRepositoryType +{ Observable fetchActiveTokenBalance(String walletAddress, Token token); + Single updateTokenBalance(String walletAddress, Token token); + Single getTokenResponse(String address, long chainId, String method); - Single checkInterface(Token[] tokens, Wallet wallet); + + Single checkInterface(Token tokens, Wallet wallet); + void setEnable(Wallet wallet, Token token, boolean isEnabled); + void setVisibilityChanged(Wallet wallet, Token token); - Single update(String address, long chainId); + + Single update(String address, long chainId, ContractType type); + Observable burnListenerObservable(String contractAddress); + Single getEthTicker(long chainId); + TokenTicker getTokenTicker(Token token); + Single fetchLatestBlockNumber(long chainId); + Token fetchToken(long chainId, String walletAddress, String address); + String getTokenImageUrl(long chainId, String address); Single storeTokens(Wallet wallet, Token[] tokens); + Single resolveENS(long chainId, String address); + void updateAssets(String wallet, Token erc721Token, List additions, List removals); + void storeAsset(String currentAddress, Token token, BigInteger tokenId, NFTAsset asset); - Token[] initNFTAssets(Wallet wallet, Token[] token); + + Token initNFTAssets(Wallet wallet, Token token); Single determineCommonType(TokenInfo tokenInfo); @@ -61,28 +67,37 @@ public interface TokenRepositoryType { void addImageUrl(long chainId, String address, String imageUrl); + void updateLocalAddress(String walletAddress); + + void deleteRealmTokens(Wallet wallet, List tcmList); + Single fetchTokenMetas(Wallet wallet, List networkFilters, AssetDefinitionService svs); Single fetchAllTokenMetas(Wallet wallet, List networkFilters, - String searchTerm); + String searchTerm); Single fetchTokensThatMayNeedUpdating(String walletAddress, List networkFilters); + Single fetchAllTokensWithBlankName(String walletAddress, List networkFilters); TokenCardMeta[] fetchTokenMetasForUpdate(Wallet wallet, List networkFilters); Realm getRealmInstance(Wallet wallet); + Realm getTickerRealmInstance(); Single fetchChainBalance(String walletAddress, long chainId); + Single fixFullNames(Wallet wallet, AssetDefinitionService svs); - + boolean isEnabled(Token newToken); + boolean hasVisibilityBeenChanged(Token token); Single> getTotalValue(String currentAddress, List networkFilters); Single> getTickerUpdateList(List networkFilter); + TokenGroup getTokenGroup(long chainId, String address, ContractType type); } diff --git a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java index c5885374d1..c53995d43e 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java @@ -35,9 +35,7 @@ import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; @@ -71,41 +69,51 @@ public TokensRealmSource(RealmManager realmManager, EthereumNetworkRepositoryTyp @Override public Single saveTokens(Wallet wallet, Token[] items) { - if (!Utils.isAddressValid(wallet.address)) { return Single.fromCallable(() -> items); } - else return Single.fromCallable(() -> { - try (Realm realm = realmManager.getRealmInstance(wallet)) - { - realm.executeTransaction(r -> { - for (Token token : items) { - if (token.tokenInfo != null && token.tokenInfo.name != null && !token.tokenInfo.name.equals(EXPIRED_CONTRACT) && token.tokenInfo.symbol != null) + if (!Utils.isAddressValid(wallet.address)) + { + return Single.fromCallable(() -> items); + } + else + { + return Single.fromCallable(() -> { + try (Realm realm = realmManager.getRealmInstance(wallet)) + { + realm.executeTransaction(r -> { + for (Token token : items) { - saveTokenLocal(r, token); + if (token.tokenInfo != null && token.tokenInfo.name != null && !token.tokenInfo.name.equals(EXPIRED_CONTRACT) && token.tokenInfo.symbol != null) + { + saveTokenLocal(r, token); + } } - } - }); - } - catch (Exception e) - { - // - } - return items; - }); + }); + } + catch (Exception e) + { + Timber.w(e); + } + return items; + }); + } } @Override - public void deleteRealmToken(long chainId, Wallet wallet, String address) + public void deleteRealmTokens(Wallet wallet, List tcmList) { try (Realm realm = realmManager.getRealmInstance(wallet)) { - realm.executeTransactionAsync(r -> { - String dbKey = databaseKey(chainId, address); - RealmToken realmToken = r.where(RealmToken.class) - .equalTo("address", dbKey) - .findFirst(); - - if (realmToken != null) + realm.executeTransaction(r -> { + for (TokenCardMeta tcm : tcmList) { - realmToken.deleteFromRealm(); + String dbKey = databaseKey(tcm.getChain(), tcm.getAddress()); + RealmToken realmToken = r.where(RealmToken.class) + .equalTo("address", dbKey) + .findFirst(); + + if (realmToken != null) + { + realmToken.deleteFromRealm(); + } } }); } @@ -174,6 +182,7 @@ private void saveTokenLocal(Realm r, Token token) case MAYBE_ERC20: case ERC721: case ERC721_LEGACY: + case ERC721_ENUMERABLE: case ERC1155: saveToken(r, token); break; @@ -282,11 +291,9 @@ public void updateNFTAssets(String wallet, Token token, List additio realm.executeTransaction(r -> { createTokenIfRequired(r, token); deleteAssets(r, token, removals); - int assetCount = updateNFTAssets(r, token, additions); - //now re-do the balance - assetCount = token.getBalanceRaw().intValue(); + updateNFTAssets(r, token, additions); - setTokenUpdateTime(r, token, assetCount); + setTokenUpdateTime(r, token); }); } catch (Exception e) @@ -307,20 +314,22 @@ private void createTokenIfRequired(Realm realm, Token token) } } - private void setTokenUpdateTime(Realm realm, Token token, int assetCount) + private void setTokenUpdateTime(Realm realm, Token token) { RealmToken realmToken = realm.where(RealmToken.class) .equalTo("address", databaseKey(token)) .findFirst(); + long tokenBalance = token.balance.longValue(); + if (realmToken != null) { - if (!realmToken.isEnabled() && !realmToken.isVisibilityChanged() && assetCount > 0) + if (!realmToken.isEnabled() && !realmToken.isVisibilityChanged() && tokenBalance > 0) { token.tokenInfo.isEnabled = true; realmToken.setEnabled(true); } - else if (!realmToken.isVisibilityChanged() && assetCount == 0) + else if (!realmToken.isVisibilityChanged() && tokenBalance == 0) { token.tokenInfo.isEnabled = false; realmToken.setEnabled(false); @@ -329,32 +338,51 @@ else if (!realmToken.isVisibilityChanged() && assetCount == 0) realmToken.setLastTxTime(System.currentTimeMillis()); realmToken.setAssetUpdateTime(System.currentTimeMillis()); - if (realmToken.getBalance() == null || !realmToken.getBalance().equals(String.valueOf(assetCount))) + if (realmToken.getBalance() == null || !realmToken.getBalance().equals(String.valueOf(tokenBalance))) { - realmToken.setBalance(String.valueOf(assetCount)); + realmToken.setBalance(String.valueOf(tokenBalance)); } } } - private int updateNFTAssets(Realm realm, Token token, List additions) throws RealmException + private void updateNFTAssets(Realm realm, Token token, List additions) throws RealmException { - if (!token.isNonFungible()) return 0; + if (!token.isNonFungible()) return; //load all the old assets Map assetMap = getNFTAssets(realm, token); - int assetCount = assetMap.size(); - for (BigInteger updatedTokenId : additions) + //create addition asset map + Map additionMap = new HashMap<>(); + + for (BigInteger tokenId : additions) { - NFTAsset asset = assetMap.get(updatedTokenId); - if (asset == null || asset.requiresReplacement()) + NFTAsset asset = assetMap.get(tokenId); + if (asset == null) asset = new NFTAsset(tokenId); + additionMap.put(tokenId, asset); + } + + Map balanceMap = token.queryAssets(additionMap); + + List deleteList = new ArrayList<>(); + + //update token assets + for (Map.Entry entry : balanceMap.entrySet()) + { + if (entry.getValue().getBalance().longValue() == 0) + { + deleteList.add(entry.getKey()); + } + else { - writeAsset(realm, token, updatedTokenId, new NFTAsset()); - if (asset == null) assetCount++; + writeAsset(realm, token, entry.getKey(), entry.getValue()); } } - return assetCount; + if (deleteList.size() > 0) + { + deleteAssets(realm, token, deleteList); + } } @Override @@ -428,6 +456,11 @@ public boolean updateTokenBalance(Wallet wallet, Token token, BigDecimal balance { boolean balanceChanged = false; String key = databaseKey(token); + if (token.getWallet() == null) + { + token.setTokenWallet(wallet.address); + } + try (Realm realm = realmManager.getRealmInstance(wallet)) { RealmToken realmToken = realm.where(RealmToken.class) @@ -450,6 +483,8 @@ public boolean updateTokenBalance(Wallet wallet, Token token, BigDecimal balance }); } + validateTokenName(realm, realmToken, token, balance); + if ((token.isERC721()) && balance.equals(BigDecimal.ZERO) && !currentBalance.equals("0")) { //only used for determining if balance is now zero @@ -507,6 +542,16 @@ else if ((!realmToken.isVisibilityChanged() && !realmToken.isEnabled()) && return balanceChanged; } + private void validateTokenName(Realm realm, RealmToken realmToken, Token token, BigDecimal balance) + { + if (TextUtils.isEmpty(token.tokenInfo.name) && TextUtils.isEmpty(token.tokenInfo.symbol) && balance.compareTo(BigDecimal.ZERO) > 0) + { + realm.executeTransaction(r -> { + realmToken.setName(Utils.formatAddress(token.tokenInfo.address)); + }); + } + } + private boolean checkEthToken(Realm realm, Token token) { if (!token.isEthereum()) return true; @@ -607,11 +652,12 @@ private void saveToken(Realm realm, Token token) throws RealmException writeAssetContract(realm, token); } - //check if name needs to be updated - if (!TextUtils.isEmpty(token.tokenInfo.name) && (TextUtils.isEmpty(realmToken.getName()) || !realmToken.getName().equals(token.tokenInfo.name))) + if (oldToken.getInterfaceSpec() != token.getInterfaceSpec()) { - realmToken.setName(token.tokenInfo.name); + realmToken.setInterfaceSpec(token.getInterfaceSpec().ordinal()); } + + checkNameUpdate(realmToken, token); } //Final check to see if the token should be visible @@ -630,6 +676,23 @@ else if (!token.isEthereum() && (token.balance.compareTo(BigDecimal.ZERO) <= 0 & } } + private void checkNameUpdate(RealmToken realmToken, Token token) + { + //check if name needs to be updated + if (!TextUtils.isEmpty(token.tokenInfo.name) && (TextUtils.isEmpty(realmToken.getName()) || !realmToken.getName().equals(token.tokenInfo.name))) + { + realmToken.setName(token.tokenInfo.name); + } + + //This will be an update from the transfer + if (token.tokenInfo.name.equalsIgnoreCase(Utils.formatAddress(token.tokenInfo.address)) || !TextUtils.isEmpty(token.tokenInfo.name)) + { + realmToken.setName(token.tokenInfo.name); + realmToken.setSymbol(token.tokenInfo.symbol); + realmToken.setDecimals(token.tokenInfo.decimals); + } + } + private void writeAssetContract(final Realm realm, Token token) { if (token == null || token.getAssetContract() == null) return; @@ -652,62 +715,38 @@ private void writeAssetContract(final Realm realm, Token token) realm.insertOrUpdate(realmNFT); } - // NFT Assets From Opensea + // NFT Assets From Opensea - assume this list is trustworthy - events will catch up with it @Override - public Token[] initNFTAssets(Wallet wallet, Token[] tokens) + public Token initNFTAssets(Wallet wallet, Token token) { + if (!token.isNonFungible()) return token; try (Realm realm = realmManager.getRealmInstance(wallet)) { realm.executeTransaction(r -> { - for (Token token : tokens) - { - if (!token.isNonFungible()) continue; - - //load all the assets from the database - Map assetMap = getNFTAssets(r, token); + //load all the assets from the database + Map assetMap = getNFTAssets(r, token); - //construct live list - //for erc1155 need to check each potential 'removal'. - //erc721 gets removed by noting token transfer - Map liveMap = token.queryAssets(assetMap); - HashSet deleteList = new HashSet<>(); - - for (BigInteger tokenId : liveMap.keySet()) - { - NFTAsset oldAsset = assetMap.get(tokenId); //may be null - NFTAsset newAsset = liveMap.get(tokenId); //never null + //run through the new assets and patch + for (Map.Entry entry : token.getTokenAssets().entrySet()) + { + NFTAsset fromOpenSea = entry.getValue(); + NFTAsset fromDataBase = assetMap.get(entry.getKey()); - if (newAsset.getBalance().compareTo(BigDecimal.ZERO) == 0) - { - deleteAssets(r, token, Collections.singletonList(tokenId)); - deleteList.add(tokenId); - } - else - { - //token updated or new - if (oldAsset != null) { newAsset.updateAsset(oldAsset); } - writeAsset(r, token, tokenId, newAsset); - } - } + fromOpenSea.updateAsset(fromDataBase); - for (BigInteger tokenId : deleteList) - { - liveMap.remove(tokenId); - } + token.getTokenAssets().put(entry.getKey(), fromOpenSea); - //update token balance & visibility - setTokenUpdateTime(r, token, liveMap.keySet().size()); - token.balance = new BigDecimal(liveMap.keySet().size()); - if (token.getTokenAssets().hashCode() != liveMap.hashCode()) //replace asset map if different - { - token.getTokenAssets().clear(); - token.getTokenAssets().putAll(liveMap); - } + //write to realm + writeAsset(r, token, entry.getKey(), fromOpenSea); } }); } + catch (Exception e) + { + Timber.w(e); + } - return tokens; + return token; } private void writeAsset(Realm realm, Token token, BigInteger tokenId, NFTAsset asset) @@ -873,7 +912,7 @@ public Single fetchTokenMetas(Wallet wallet, List network { if (rootChainTokenCards.contains(t.getChainId())) { - rootChainTokenCards.remove((Long)t.getChainId()); + rootChainTokenCards.remove(t.getChainId()); } else { @@ -1367,6 +1406,7 @@ public static String convertStringBalance(String balance, ContractType type) case ERC721_UNDETERMINED: case ERC721: case ERC721_LEGACY: + case ERC721_ENUMERABLE: case ERC1155: default: return balance; @@ -1573,6 +1613,7 @@ public TokenGroup getTokenGroup(long chainId, String address, ContractType type) return tg; case ERC721: + case ERC721_ENUMERABLE: case ERC875_LEGACY: case ERC875: case ERC1155: diff --git a/app/src/main/java/com/alphawallet/app/repository/TransactionRepository.java b/app/src/main/java/com/alphawallet/app/repository/TransactionRepository.java index 645ff1cf44..3ebe2b06fd 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TransactionRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TransactionRepository.java @@ -24,263 +24,282 @@ import io.reactivex.schedulers.Schedulers; import io.realm.Realm; -public class TransactionRepository implements TransactionRepositoryType { +public class TransactionRepository implements TransactionRepositoryType +{ - private final String TAG = "TREPO"; - private final EthereumNetworkRepositoryType networkRepository; - private final AccountKeystoreService accountKeystoreService; + private final String TAG = "TREPO"; + private final EthereumNetworkRepositoryType networkRepository; + private final AccountKeystoreService accountKeystoreService; private final TransactionLocalSource inDiskCache; private final TransactionsService transactionsService; - public TransactionRepository( - EthereumNetworkRepositoryType networkRepository, - AccountKeystoreService accountKeystoreService, - TransactionLocalSource inDiskCache, - TransactionsService transactionsService) { - this.networkRepository = networkRepository; - this.accountKeystoreService = accountKeystoreService; - this.inDiskCache = inDiskCache; - this.transactionsService = transactionsService; - } - - @Override - public Transaction fetchCachedTransaction(String walletAddr, String hash) - { - Wallet wallet = new Wallet(walletAddr); - return inDiskCache.fetchTransaction(wallet, hash); - } - - @Override - public long fetchTxCompletionTime(String walletAddr, String hash) - { - Wallet wallet = new Wallet(walletAddr); - return inDiskCache.fetchTxCompletionTime(wallet, hash); - } - - @Override - public Single resendTransaction(Wallet from, String to, BigInteger subunitAmount, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, byte[] data, long chainId) - { - final Web3j web3j = getWeb3jService(chainId); - final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); - - return accountKeystoreService.signTransaction(from, to, subunitAmount, useGasPrice, gasLimit, nonce.longValue(), data, chainId) - .flatMap(signedMessage -> Single.fromCallable( () -> { - if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) - { - throw new Exception(signedMessage.failMessage); - } - EthSendTransaction raw = web3j - .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) - .send(); - if (raw.hasError()) - { - throw new Exception(raw.getError().getMessage()); - } - return raw.getTransactionHash(); - })) - .flatMap(txHash -> storeUnconfirmedTransaction(from, txHash, to, subunitAmount, nonce, useGasPrice, gasLimit, chainId, data != null ? Numeric.toHexString(data) : "0x")) - .subscribeOn(Schedulers.io()); - } - - @Override - public Single create1559TransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasLimit, BigInteger maxFeePerGas, BigInteger maxPriorityFee, long nonce, byte[] data, long chainId) { - final Web3j web3j = getWeb3jService(chainId); - - TransactionData txData = new TransactionData(); - - return getNonceForTransaction(web3j, from.address, nonce) - .flatMap(txNonce -> { - txData.nonce = txNonce; - return accountKeystoreService.signTransactionEIP1559(from, toAddress, subunitAmount, gasLimit, maxFeePerGas, maxPriorityFee, txNonce.longValue(), data, chainId); - }) - .flatMap(signedMessage -> Single.fromCallable( () -> { - if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) - { - throw new Exception(signedMessage.failMessage); - } - txData.signature = Numeric.toHexString(signedMessage.signature); - EthSendTransaction raw = web3j - .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) - .send(); - if (raw.hasError()) { - throw new Exception(raw.getError().getMessage()); - } - txData.txHash = raw.getTransactionHash(); - return txData; - })) - .flatMap(tx -> storeUnconfirmedTransaction(from, tx, toAddress, subunitAmount, tx.nonce, maxFeePerGas, maxPriorityFee, gasLimit, chainId, data != null ? Numeric.toHexString(data) : "0x", "")) - .subscribeOn(Schedulers.io()); - } - - @Override - public Single createTransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId) { - final Web3j web3j = getWeb3jService(chainId); - final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); - - TransactionData txData = new TransactionData(); - - return getNonceForTransaction(web3j, from.address, nonce) - .flatMap(txNonce -> { - txData.nonce = txNonce; - return accountKeystoreService.signTransaction(from, toAddress, subunitAmount, useGasPrice, gasLimit, txNonce.longValue(), data, chainId); - }) - .flatMap(signedMessage -> Single.fromCallable( () -> { - if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) - { - throw new Exception(signedMessage.failMessage); - } - txData.signature = Numeric.toHexString(signedMessage.signature); - EthSendTransaction raw = web3j - .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) - .send(); - if (raw.hasError()) { - throw new Exception(raw.getError().getMessage()); - } - txData.txHash = raw.getTransactionHash(); - return txData; - })) - .flatMap(tx -> storeUnconfirmedTransaction(from, tx, toAddress, subunitAmount, tx.nonce, useGasPrice, gasLimit, chainId, data != null ? Numeric.toHexString(data) : "0x", "")) - .subscribeOn(Schedulers.io()); - } - - /** - * * Given a Web3Transaction, return a signature. Note that we can't fix up nonce, gas price or limit; - * * This is a request to sign a transaction from an external source - - * * presumably that external source will broadcast the transaction; together with this signature - * - * @param from - * @param toAddress - * @param subunitAmount - * @param gasPrice - * @param gasLimit - * @param nonce - * @param data - * @param chainId - * @return - */ - @Override - public Single getSignatureForTransaction(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId) { - final Web3j web3j = getWeb3jService(chainId); - final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); - TransactionData txData = new TransactionData(); - - return getNonceForTransaction(web3j, from.address, nonce) - .flatMap(txNonce -> { - txData.nonce = txNonce; - return accountKeystoreService.signTransaction(from, toAddress, subunitAmount, useGasPrice, gasLimit, txNonce.longValue(), data, chainId); - }) - .flatMap(signedMessage -> Single.fromCallable( () -> { - if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) - { - throw new Exception(signedMessage.failMessage); - } - txData.signature = Numeric.toHexString(signedMessage.signature); - return txData; - })); - } - - private BigInteger gasPriceForNode(long chainId, BigInteger gasPrice) - { - if (EthereumNetworkRepository.hasGasOverride(chainId)) return EthereumNetworkRepository.gasOverrideValue(chainId); - else return gasPrice; - } - - //EIP1559 - private Single storeUnconfirmedTransaction(Wallet from, TransactionData txData, String toAddress, BigInteger value, BigInteger nonce, BigInteger maxFeePerGas, BigInteger maxPriorityFee, BigInteger gasLimit, long chainId, String data, String contractAddr) - { - return Single.fromCallable(() -> { - Transaction newTx = new Transaction(txData.txHash, "0", "0", System.currentTimeMillis()/1000, nonce.intValue(), from.address, toAddress, value.toString(10), "0", "0", maxFeePerGas.toString(10), - maxPriorityFee.toString(10), data, - gasLimit.toString(10), chainId, contractAddr); - inDiskCache.putTransaction(from, newTx); - transactionsService.markPending(newTx); - - return txData; - }); - } - - private Single storeUnconfirmedTransaction(Wallet from, TransactionData txData, String toAddress, BigInteger value, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, long chainId, String data, String contractAddr) - { - return Single.fromCallable(() -> { - Transaction newTx = new Transaction(txData.txHash, "0", "0", System.currentTimeMillis()/1000, nonce.intValue(), from.address, toAddress, value.toString(10), "0", gasPrice.toString(10), data, - gasLimit.toString(10), chainId, contractAddr); - //newTx.completeSetup(from.address); - inDiskCache.putTransaction(from, newTx); - transactionsService.markPending(newTx); - - return txData; - }); - } - - private Single storeUnconfirmedTransaction(Wallet from, String txHash, String toAddress, BigInteger value, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, long chainId, String data) - { - return Single.fromCallable(() -> { - - Transaction newTx = new Transaction(txHash, "0", "0", System.currentTimeMillis()/1000, nonce.intValue(), from.address, toAddress, value.toString(10), "0", gasPrice.toString(10), data, - gasLimit.toString(10), chainId, ""); - //newTx.completeSetup(from.address); - inDiskCache.putTransaction(from, newTx); - transactionsService.markPending(newTx); - - return txHash; - }); - } - - @Override - public Single getSignature(Wallet wallet, Signable message, long chainId) { - return accountKeystoreService.signMessage(wallet, message, chainId); - } - - @Override - public Single getSignatureFast(Wallet wallet, String password, byte[] message, long chainId) { - return accountKeystoreService.signTransactionFast(wallet, password, message, chainId); - } - - @Override - public Single fetchCachedTransactionMetas(Wallet wallet, List networkFilters, long fetchTime, int fetchLimit) - { - return inDiskCache.fetchActivityMetas(wallet, networkFilters, fetchTime, fetchLimit); - } - - @Override - public Single fetchCachedTransactionMetas(Wallet wallet, long chainId, String tokenAddress, int historyCount) - { - return inDiskCache.fetchActivityMetas(wallet, chainId, tokenAddress, historyCount); - } - - @Override - public Single fetchEventMetas(Wallet wallet, List networkFilters) - { - return inDiskCache.fetchEventMetas(wallet, networkFilters); - } - - @Override - public Realm getRealmInstance(Wallet wallet) - { - return inDiskCache.getRealmInstance(wallet); - } - - @Override - public RealmAuxData fetchCachedEvent(String walletAddress, String eventKey) - { - return inDiskCache.fetchEvent(walletAddress, eventKey); - } - - @Override - public void restartService() - { - transactionsService.startUpdateCycle(); - } - - private Single getNonceForTransaction(Web3j web3j, String wallet, long nonce) - { - if (nonce != -1) //use supplied nonce - { - return Single.fromCallable(() -> BigInteger.valueOf(nonce)); - } - else - { - return networkRepository.getLastTransactionNonce(web3j, wallet); - } - } + public TransactionRepository( + EthereumNetworkRepositoryType networkRepository, + AccountKeystoreService accountKeystoreService, + TransactionLocalSource inDiskCache, + TransactionsService transactionsService) + { + this.networkRepository = networkRepository; + this.accountKeystoreService = accountKeystoreService; + this.inDiskCache = inDiskCache; + this.transactionsService = transactionsService; + } + + @Override + public Transaction fetchCachedTransaction(String walletAddr, String hash) + { + Wallet wallet = new Wallet(walletAddr); + return inDiskCache.fetchTransaction(wallet, hash); + } + + @Override + public long fetchTxCompletionTime(String walletAddr, String hash) + { + Wallet wallet = new Wallet(walletAddr); + return inDiskCache.fetchTxCompletionTime(wallet, hash); + } + + @Override + public Single resendTransaction(Wallet from, String to, BigInteger subunitAmount, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, byte[] data, long chainId) + { + final Web3j web3j = getWeb3jService(chainId); + final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); + + return accountKeystoreService.signTransaction(from, to, subunitAmount, useGasPrice, gasLimit, nonce.longValue(), data, chainId) + .flatMap(signedMessage -> Single.fromCallable(() -> { + if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) + { + throw new Exception(signedMessage.failMessage); + } + EthSendTransaction raw = web3j + .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) + .send(); + if (raw.hasError()) + { + throw new Exception(raw.getError().getMessage()); + } + return raw.getTransactionHash(); + })) + .flatMap(txHash -> storeUnconfirmedTransaction(from, txHash, to, subunitAmount, nonce, useGasPrice, gasLimit, chainId, data != null ? Numeric.toHexString(data) : "0x")) + .subscribeOn(Schedulers.io()); + } + + @Override + public Single create1559TransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasLimit, BigInteger maxFeePerGas, + BigInteger maxPriorityFee, long nonce, byte[] data, long chainId) + { + final Web3j web3j = getWeb3jService(chainId); + + TransactionData txData = new TransactionData(); + + return getNonceForTransaction(web3j, from.address, nonce) + .flatMap(txNonce -> { + txData.nonce = txNonce; + return accountKeystoreService.signTransactionEIP1559(from, toAddress, subunitAmount, gasLimit, maxFeePerGas, maxPriorityFee, txNonce.longValue(), data, chainId); + }) + .flatMap(signedMessage -> Single.fromCallable(() -> { + if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) + { + throw new Exception(signedMessage.failMessage); + } + txData.signature = Numeric.toHexString(signedMessage.signature); + EthSendTransaction raw = web3j + .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) + .send(); + if (raw.hasError()) + { + throw new Exception(raw.getError().getMessage()); + } + txData.txHash = raw.getTransactionHash(); + return txData; + })) + .flatMap(tx -> storeUnconfirmedTransaction(from, tx, toAddress, subunitAmount, tx.nonce, maxFeePerGas, maxPriorityFee, gasLimit, chainId, + data != null ? Numeric.toHexString(data) : "0x", "")) + .subscribeOn(Schedulers.io()); + } + + @Override + public Single createTransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId) + { + final Web3j web3j = getWeb3jService(chainId); + final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); + + TransactionData txData = new TransactionData(); + + return getNonceForTransaction(web3j, from.address, nonce) + .flatMap(txNonce -> { + txData.nonce = txNonce; + return accountKeystoreService.signTransaction(from, toAddress, subunitAmount, useGasPrice, gasLimit, txNonce.longValue(), data, chainId); + }) + .flatMap(signedMessage -> Single.fromCallable(() -> { + if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) + { + throw new Exception(signedMessage.failMessage); + } + txData.signature = Numeric.toHexString(signedMessage.signature); + EthSendTransaction raw = web3j + .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) + .send(); + if (raw.hasError()) + { + throw new Exception(raw.getError().getMessage()); + } + txData.txHash = raw.getTransactionHash(); + return txData; + })) + .flatMap(tx -> storeUnconfirmedTransaction(from, tx, toAddress, subunitAmount, tx.nonce, useGasPrice, gasLimit, chainId, + data != null ? Numeric.toHexString(data) : "0x", "")) + .subscribeOn(Schedulers.io()); + } + + /** + * * Given a Web3Transaction, return a signature. Note that we can't fix up nonce, gas price or limit; + * * This is a request to sign a transaction from an external source - + * * presumably that external source will broadcast the transaction; together with this signature + * + * @param from + * @param toAddress + * @param subunitAmount + * @param gasPrice + * @param gasLimit + * @param nonce + * @param data + * @param chainId + * @return + */ + @Override + public Single getSignatureForTransaction(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, + byte[] data, long chainId) + { + final Web3j web3j = getWeb3jService(chainId); + final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); + TransactionData txData = new TransactionData(); + + return getNonceForTransaction(web3j, from.address, nonce) + .flatMap(txNonce -> { + txData.nonce = txNonce; + return accountKeystoreService.signTransaction(from, toAddress, subunitAmount, useGasPrice, gasLimit, txNonce.longValue(), data, chainId); + }) + .flatMap(signedMessage -> Single.fromCallable(() -> { + if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) + { + throw new Exception(signedMessage.failMessage); + } + txData.signature = Numeric.toHexString(signedMessage.signature); + return txData; + })); + } + + private BigInteger gasPriceForNode(long chainId, BigInteger gasPrice) + { + if (EthereumNetworkRepository.hasGasOverride(chainId)) return EthereumNetworkRepository.gasOverrideValue(chainId); + else return gasPrice; + } + + //EIP1559 + private Single storeUnconfirmedTransaction(Wallet from, TransactionData txData, String toAddress, BigInteger value, BigInteger nonce, BigInteger maxFeePerGas, + BigInteger maxPriorityFee, BigInteger gasLimit, long chainId, String data, String contractAddr) + { + return Single.fromCallable(() -> { + Transaction newTx = new Transaction(txData.txHash, "0", "0", System.currentTimeMillis() / 1000, nonce.intValue(), from.address, toAddress, + value.toString(10), "0", "0", maxFeePerGas.toString(10), + maxPriorityFee.toString(10), data, + gasLimit.toString(10), chainId, contractAddr); + inDiskCache.putTransaction(from, newTx); + transactionsService.markPending(newTx); + + return txData; + }); + } + + private Single storeUnconfirmedTransaction(Wallet from, TransactionData txData, String toAddress, BigInteger value, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, + long chainId, String data, String contractAddr) + { + return Single.fromCallable(() -> { + Transaction newTx = new Transaction(txData.txHash, "0", "0", System.currentTimeMillis() / 1000, nonce.intValue(), from.address, toAddress, + value.toString(10), "0", gasPrice.toString(10), data, + gasLimit.toString(10), chainId, contractAddr); + //newTx.completeSetup(from.address); + inDiskCache.putTransaction(from, newTx); + transactionsService.markPending(newTx); + + return txData; + }); + } + + private Single storeUnconfirmedTransaction(Wallet from, String txHash, String toAddress, BigInteger value, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, + long chainId, String data) + { + return Single.fromCallable(() -> { + + Transaction newTx = new Transaction(txHash, "0", "0", System.currentTimeMillis() / 1000, nonce.intValue(), from.address, + toAddress, value.toString(10), "0", gasPrice.toString(10), data, + gasLimit.toString(10), chainId, ""); + //newTx.completeSetup(from.address); + inDiskCache.putTransaction(from, newTx); + transactionsService.markPending(newTx); + + return txHash; + }); + } + + @Override + public Single getSignature(Wallet wallet, Signable message) + { + return accountKeystoreService.signMessage(wallet, message); + } + + @Override + public Single getSignatureFast(Wallet wallet, String password, byte[] message) + { + return accountKeystoreService.signMessageFast(wallet, password, message); + } + + @Override + public Single fetchCachedTransactionMetas(Wallet wallet, List networkFilters, long fetchTime, int fetchLimit) + { + return inDiskCache.fetchActivityMetas(wallet, networkFilters, fetchTime, fetchLimit); + } + + @Override + public Single fetchCachedTransactionMetas(Wallet wallet, long chainId, String tokenAddress, int historyCount) + { + return inDiskCache.fetchActivityMetas(wallet, chainId, tokenAddress, historyCount); + } + + @Override + public Single fetchEventMetas(Wallet wallet, List networkFilters) + { + return inDiskCache.fetchEventMetas(wallet, networkFilters); + } + + @Override + public Realm getRealmInstance(Wallet wallet) + { + return inDiskCache.getRealmInstance(wallet); + } + + @Override + public RealmAuxData fetchCachedEvent(String walletAddress, String eventKey) + { + return inDiskCache.fetchEvent(walletAddress, eventKey); + } + + @Override + public void restartService() + { + transactionsService.startUpdateCycle(); + } + + private Single getNonceForTransaction(Web3j web3j, String wallet, long nonce) + { + if (nonce != -1) //use supplied nonce + { + return Single.fromCallable(() -> BigInteger.valueOf(nonce)); + } + else + { + return networkRepository.getLastTransactionNonce(web3j, wallet); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/repository/TransactionRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/TransactionRepositoryType.java index 9cc3abfe0c..957f0ca404 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TransactionRepositoryType.java +++ b/app/src/main/java/com/alphawallet/app/repository/TransactionRepositoryType.java @@ -14,26 +14,33 @@ import io.reactivex.Single; import io.realm.Realm; -public interface TransactionRepositoryType { - Single createTransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId); - Single create1559TransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasLimit, BigInteger maxFeePerGas, BigInteger maxPriorityFee, long nonce, byte[] data, long chainId); - Single getSignatureForTransaction(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId); +public interface TransactionRepositoryType +{ + Single createTransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId); - Single getSignature(Wallet wallet, Signable message, long chainId); - Single getSignatureFast(Wallet wallet, String password, byte[] message, long chainId); + Single create1559TransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasLimit, BigInteger maxFeePerGas, BigInteger maxPriorityFee, long nonce, byte[] data, long chainId); + + Single getSignatureForTransaction(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId); + + Single getSignature(Wallet wallet, Signable message); + + Single getSignatureFast(Wallet wallet, String password, byte[] message); Transaction fetchCachedTransaction(String walletAddr, String hash); - long fetchTxCompletionTime(String walletAddr, String hash); - Single resendTransaction(Wallet from, String to, BigInteger subunitAmount, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, byte[] data, long chainId); + long fetchTxCompletionTime(String walletAddr, String hash); + + Single resendTransaction(Wallet from, String to, BigInteger subunitAmount, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, byte[] data, long chainId); Single fetchCachedTransactionMetas(Wallet wallet, List networkFilters, long fetchTime, int fetchLimit); - Single fetchCachedTransactionMetas(Wallet wallet, long chainId, String tokenAddress, int historyCount); - Single fetchEventMetas(Wallet wallet, List networkFilters); - Realm getRealmInstance(Wallet wallet); + Single fetchCachedTransactionMetas(Wallet wallet, long chainId, String tokenAddress, int historyCount); + + Single fetchEventMetas(Wallet wallet, List networkFilters); + + Realm getRealmInstance(Wallet wallet); - RealmAuxData fetchCachedEvent(String walletAddress, String eventKey); + RealmAuxData fetchCachedEvent(String walletAddress, String eventKey); void restartService(); } diff --git a/app/src/main/java/com/alphawallet/app/repository/WalletDataRealmSource.java b/app/src/main/java/com/alphawallet/app/repository/WalletDataRealmSource.java index d2af687b20..43fd722d07 100644 --- a/app/src/main/java/com/alphawallet/app/repository/WalletDataRealmSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/WalletDataRealmSource.java @@ -1,6 +1,7 @@ package com.alphawallet.app.repository; import android.text.TextUtils; +import android.util.Pair; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; @@ -43,7 +44,7 @@ public Single populateWalletData(Wallet[] keystoreWallets, KeyService Map walletList; try (Realm realm = realmManager.getWalletDataRealmInstance()) { - walletList = loadOrCreateKeyRealmDB(realm, keystoreWallets); //call has action on upgrade to new UX + walletList = loadOrCreateKeyRealmDB(realm, keystoreWallets, keyService); //call has action on upgrade to new UX //Add additional - non critical wallet data. This database can be voided for upgrade if required for (Wallet wallet : walletList.values()) { @@ -53,19 +54,16 @@ public Single populateWalletData(Wallet[] keystoreWallets, KeyService composeWallet(wallet, data); } - - //TODO: Make this a manual process. - //recoverLostWallets(realm, keystoreWallets, walletList, keyService); } - migrateWalletTypeData(walletList); + migrateWalletTypeData(walletList, keyService); Timber.tag("RealmDebug").d("populate %s", walletList.size()); return walletList.values().toArray(new Wallet[0]); }); } - private Map loadOrCreateKeyRealmDB(Realm realm, Wallet[] wallets) + private Map loadOrCreateKeyRealmDB(Realm realm, Wallet[] wallets, KeyService keyService) { Map walletList = new HashMap<>(); List keyStoreList = walletArrayToAddressList(wallets); @@ -75,6 +73,8 @@ private Map loadOrCreateKeyRealmDB(Realm realm, Wallet[] wallets .sort("dateAdded", Sort.ASCENDING) .findAll(); + List walletUpdates = new ArrayList<>(); + if (realmKeyTypes.size() > 0) { //Load fixed wallet data: wallet type, creation and backup times @@ -87,23 +87,39 @@ private Map loadOrCreateKeyRealmDB(Realm realm, Wallet[] wallets continue; } + if (w.type == WalletType.KEYSTORE_LEGACY && !testLegacyCipher(w, keyService)) + { + w.type = WalletType.KEYSTORE; + walletUpdates.add(w); + } + walletList.put(w.address.toLowerCase(), w); } } else //only zero on upgrade from v2.01.3 and lower (pre-HD key) { - realm.executeTransaction(r -> { - for (Wallet wallet : wallets) + for (Wallet wallet : wallets) + { + wallet.authLevel = KeyService.AuthenticationLevel.TEE_NO_AUTHENTICATION; + if (testLegacyCipher(wallet, keyService)) { - wallet.authLevel = KeyService.AuthenticationLevel.TEE_NO_AUTHENTICATION; wallet.type = WalletType.KEYSTORE_LEGACY; - storeKeyData(wallet, r); - walletList.put(wallet.address.toLowerCase(), wallet); } - }); + else + { + wallet.type = WalletType.KEYSTORE; + } + walletList.put(wallet.address.toLowerCase(), wallet); + walletUpdates.add(wallet); + } } - Timber.tag("RealmDebug").d("loadorcreate " + walletList.size()); + if (walletUpdates.size() > 0) + { + storeWallets(realm, walletUpdates.toArray(new Wallet[0])); + } + + Timber.tag("RealmDebug").d("loadorcreate %s", walletList.size()); return walletList; } @@ -147,23 +163,32 @@ private String balance(RealmWalletData data) else return value; } - public Single storeWallets(Wallet[] wallets) { + public Single storeWallets(Wallet[] wallets) + { return Single.fromCallable(() -> { - try (Realm realm = realmManager.getWalletDataRealmInstance()) { - - realm.executeTransaction(r -> { - for (Wallet wallet : wallets) - { - storeKeyData(wallet, r); - } - }); - } catch (Exception e) { + try (Realm realm = realmManager.getWalletDataRealmInstance()) + { + storeWallets(realm, wallets); + } + catch (Exception e) + { Timber.e(e, "storeWallets: %s", e.getMessage()); } return wallets; }); } + private void storeWallets(Realm realm, Wallet[] wallets) + { + realm.executeTransaction(r -> { + for (Wallet wallet : wallets) + { + storeKeyData(wallet, r); + storeWalletData(wallet, r); + } + }); + } + public Single storeWallet(Wallet wallet) { return deleteWallet(wallet) //refresh data .flatMap(deletedWallet -> Single.fromCallable(() -> { @@ -180,7 +205,7 @@ public void updateWalletData(Wallet wallet, Realm.Transaction.OnSuccess onSucces realm.executeTransactionAsync(r -> { storeKeyData(wallet, r); storeWalletData(wallet, r); - Timber.tag("RealmDebug").d("storedKeydata " + wallet.address); + Timber.tag("RealmDebug").d("storedKeydata %s", wallet.address); }, onSuccess); } catch (Exception e) @@ -219,7 +244,7 @@ public void updateWalletItem(Wallet wallet, WalletItem item, Realm.Transaction.O r.insertOrUpdate(walletData); } - Timber.tag("RealmDebug").d("storedKeydata " + wallet.address); + Timber.tag("RealmDebug").d("storedKeydata %s", wallet.address); }, onSuccess); } @@ -483,9 +508,16 @@ private void storeWalletData(Wallet wallet, Realm r) r.insertOrUpdate(item); } + private boolean testLegacyCipher(Wallet w, KeyService service) + { + //test for legacy cipher, any failure we know it's a KEYSTORE + Pair cipherTest = service.testCipher(w.address, KeyService.LEGACY_CIPHER_ALGORITHM); + return cipherTest.first == KeyService.KeyExceptionType.SUCCESSFUL_DECODE; + } + //One-time removal of the WalletTypeRealmInstance usage - this extra database was a // workaround for an issue that has since been fixed correctly. - private void migrateWalletTypeData(Map walletList) + private void migrateWalletTypeData(Map walletList, KeyService service) { Map walletTypeData = new HashMap<>(); @@ -498,7 +530,14 @@ private void migrateWalletTypeData(Map walletList) for (RealmKeyType rk : rr) { Wallet w = composeKeyType(rk); - if (w != null) walletTypeData.put(w.address.toLowerCase(), w); + if (w != null) + { + walletTypeData.put(w.address.toLowerCase(), w); + if (w.type == WalletType.KEYSTORE_LEGACY && !testLegacyCipher(w, service)) + { + w.type = WalletType.KEYSTORE; + } + } } } diff --git a/app/src/main/java/com/alphawallet/app/router/CoinbasePayRouter.java b/app/src/main/java/com/alphawallet/app/router/CoinbasePayRouter.java new file mode 100644 index 0000000000..ad98deae04 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/router/CoinbasePayRouter.java @@ -0,0 +1,44 @@ +package com.alphawallet.app.router; + +import android.app.Activity; +import android.content.Intent; + +import com.alphawallet.app.R; +import com.alphawallet.app.ui.CoinbasePayActivity; + +public class CoinbasePayRouter +{ + /** + * @param activity - Calling activity + * @param tokenSymbol - Token symbol of the asset you wish to purchase, e.g. "ETH", "USDC" + */ + public void buyAsset(Activity activity, String tokenSymbol) + { + Intent intent = new Intent(activity, CoinbasePayActivity.class); + intent.putExtra("asset", tokenSymbol); + intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.hold); + } + + /** + * @param activity - Calling activity + * @param blockchain - Select from supported chains from `CoinbasePayRepository.Blockchains` + */ + public void buyFromSelectedChain(Activity activity, String blockchain) + { + Intent intent = new Intent(activity, CoinbasePayActivity.class); + intent.putExtra("blockchain", blockchain); + intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.hold); + } + + public void open(Activity activity) + { + Intent intent = new Intent(activity, CoinbasePayActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.hold); + } +} diff --git a/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java b/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java index a80fd1d76c..774b326d22 100644 --- a/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java +++ b/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java @@ -4,11 +4,11 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.util.Log; import com.alphawallet.app.C; -import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.ui.AssetDisplayActivity; import com.alphawallet.app.ui.Erc20DetailActivity; import com.alphawallet.app.ui.NFTActivity; @@ -55,4 +55,14 @@ public void open(Activity activity, Token token, Wallet wallet) intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); activity.startActivityForResult(intent, C.TERMINATE_ACTIVITY); } + + public void openLegacyToken(Activity context, Token token, Wallet wallet) + { + Intent intent = new Intent(context, AssetDisplayActivity.class); + intent.putExtra(C.EXTRA_CHAIN_ID, token.tokenInfo.chainId); + intent.putExtra(C.EXTRA_ADDRESS, token.getAddress()); + intent.putExtra(C.Key.WALLET, wallet); + intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + context.startActivityForResult(intent, C.TERMINATE_ACTIVITY); + } } diff --git a/app/src/main/java/com/alphawallet/app/service/AWHttpService.java b/app/src/main/java/com/alphawallet/app/service/AWHttpService.java index 713d999e97..6170a54bcd 100644 --- a/app/src/main/java/com/alphawallet/app/service/AWHttpService.java +++ b/app/src/main/java/com/alphawallet/app/service/AWHttpService.java @@ -129,7 +129,7 @@ protected InputStream performIO(String request) throws IOException okhttp3.Request httpRequest = new okhttp3.Request.Builder().url(url).headers(headers).post(requestBody).build(); - okhttp3.Response response; + okhttp3.Response response = null; try { @@ -144,14 +144,14 @@ protected InputStream performIO(String request) throws IOException } else { - Timber.d("performIO: throw SocketTimeoutException"); throw new SocketTimeoutException(); } } //TODO: Also check java.io.InterruptedIOException - if (response.code()/100 == 4) //rate limited + if (response.code() / 100 == 4) //rate limited { + response.close(); return trySecondaryNode(request); } @@ -162,10 +162,9 @@ private InputStream trySecondaryNode(String request) throws IOException { Timber.d("trySecondaryNode: "); RequestBody requestBody = RequestBody.create(request, JSON_MEDIA_TYPE); - Headers headers = buildHeaders(); okhttp3.Request httpRequest = - new okhttp3.Request.Builder().url(secondaryUrl).headers(headers).post(requestBody).build(); + new okhttp3.Request.Builder().url(secondaryUrl).post(requestBody).build(); okhttp3.Response response; @@ -191,6 +190,7 @@ private InputStream processNodeResponse(Response response, String request, boole } else if (!useSecondaryNode && secondaryUrl != null) { + response.close(); return trySecondaryNode(request); } else @@ -260,4 +260,10 @@ public HashMap getHeaders() { @Override public void close() throws IOException {} + + @Override + public String getUrl() + { + return this.url; + } } diff --git a/app/src/main/java/com/alphawallet/app/service/AccountKeystoreService.java b/app/src/main/java/com/alphawallet/app/service/AccountKeystoreService.java index 5fdae0a9f3..cf35e7e936 100644 --- a/app/src/main/java/com/alphawallet/app/service/AccountKeystoreService.java +++ b/app/src/main/java/com/alphawallet/app/service/AccountKeystoreService.java @@ -79,14 +79,12 @@ Single signTransactionEIP1559( Single signMessage( Wallet signer, - Signable message, - long chainId); + Signable messaged); - Single signTransactionFast( + Single signMessageFast( Wallet signer, String password, - byte[] message, - long chainId); + byte[] message); /** * Check if there is an address in the keystore diff --git a/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java b/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java index 82f01c01a9..3b95c76ec2 100644 --- a/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java +++ b/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java @@ -1,8 +1,9 @@ package com.alphawallet.app.service; -import android.util.Log; +import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; +import static com.alphawallet.token.tools.ParseMagicLink.currencyLink; +import static com.alphawallet.token.tools.ParseMagicLink.spawnable; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Ticket; @@ -33,10 +34,6 @@ import okhttp3.RequestBody; import timber.log.Timber; -import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; -import static com.alphawallet.token.tools.ParseMagicLink.currencyLink; -import static com.alphawallet.token.tools.ParseMagicLink.spawnable; - public class AlphaWalletService { private final OkHttpClient httpClient; @@ -109,6 +106,8 @@ public XMLDsigDescriptor checkTokenScriptSignature(File tokenScriptFile) String result = response.body().string(); JsonObject obj = gson.fromJson(result, JsonObject.class); + if (obj.has("error") || !obj.has("result")) return dsigDescriptor; + String queryResult = obj.get("result").getAsString(); if (queryResult.equals(XML_VERIFIER_PASS)) { @@ -234,7 +233,7 @@ private Single sendFeemasterTransaction( } catch (Exception e) { - e.printStackTrace(); + Timber.e(e); } return result; @@ -324,7 +323,7 @@ public Single checkFeemasterService(String url, long chainId, String ad } catch (Exception e) { - e.printStackTrace(); + Timber.e(e); } return result; diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index 0692453e25..d9287b6642 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -23,20 +23,16 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.ContractLocator; -import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.FragmentMessenger; +import com.alphawallet.app.entity.QueryResponse; import com.alphawallet.app.entity.TokenLocator; -import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.nftassets.NFTAsset; -import com.alphawallet.app.entity.tokens.ERC721Token; import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.entity.tokens.TokenFactory; import com.alphawallet.app.entity.tokenscript.EventUtils; import com.alphawallet.app.entity.tokenscript.TokenScriptFile; import com.alphawallet.app.entity.tokenscript.TokenscriptFunction; import com.alphawallet.app.repository.TokenLocalSource; import com.alphawallet.app.repository.TokensRealmSource; -import com.alphawallet.app.repository.TransactionRepositoryType; import com.alphawallet.app.repository.entity.RealmAuxData; import com.alphawallet.app.repository.entity.RealmCertificateData; import com.alphawallet.app.repository.entity.RealmTokenScriptData; @@ -59,6 +55,7 @@ import com.alphawallet.token.entity.TokenscriptContext; import com.alphawallet.token.entity.TokenscriptElement; import com.alphawallet.token.entity.TransactionResult; +import com.alphawallet.token.entity.ViewType; import com.alphawallet.token.entity.XMLDsigDescriptor; import com.alphawallet.token.tools.TokenDefinition; @@ -83,7 +80,6 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; -import java.math.BigDecimal; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URLEncoder; @@ -112,8 +108,6 @@ import io.realm.Sort; import io.realm.exceptions.RealmException; import io.realm.exceptions.RealmPrimaryKeyConstraintException; -import okhttp3.OkHttpClient; -import okhttp3.Request; import timber.log.Timber; @@ -130,9 +124,12 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface private static final String ASSET_DEFINITION_DB = "ASSET-db.realm"; private static final String BUNDLED_SCRIPT = "bundled"; private static final long CHECK_TX_LOGS_INTERVAL = 20; + private static final String EIP5169_ISSUER = "EIP5169-IPFS"; + private static final String EIP5169_CERTIFIER = "Smart Token Labs"; + private static final String EIP5169_KEY_OWNER = "Contract Owner"; //TODO Source this from the contract via owner() private final Context context; - private final OkHttpClient okHttpClient; + private final IPFSServiceType ipfsService; private final Map assetChecked; //Mapping of contract address to when they were last fetched from server private FileObserver fileObserver; //Observer which scans the override directory waiting for file change @@ -142,7 +139,6 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface private final TokensService tokensService; private final TokenLocalSource tokenLocalSource; private final AlphaWalletService alphaWalletService; - private final TransactionRepositoryType transactionRepository; private TokenDefinition cachedDefinition = null; private final ConcurrentHashMap eventList = new ConcurrentHashMap<>(); //List of events built during file load private final Semaphore assetLoadingLock; // used to block if someone calls getAssetDefinitionASync() while loading @@ -155,38 +151,39 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface @Nullable private Disposable checkEventDisposable; - /* Designed with the assmuption that only a single instance of this class at any given time - * ^^ The "service" part of AssetDefinitionService is the keyword here. - * This is shorthand in the project to indicate this is a singleton that other classes inject. - * This is the design pattern of the app. See class RepositoriesModule for constructors which are called at App init only */ - public AssetDefinitionService(OkHttpClient client, Context ctx, NotificationService svs, - RealmManager rm, TokensService tokensService, - TokenLocalSource trs, TransactionRepositoryType trt, - AlphaWalletService alphaService) + /* Designed with the assumption that only a single instance of this class at any given time + * ^^ The "service" part of AssetDefinitionService is the keyword here. + * This is shorthand in the project to indicate this is a singleton that other classes inject. + * This is the design pattern of the app. See class RepositoriesModule for constructors which are called at App init only */ + public AssetDefinitionService (IPFSServiceType ipfsSvs, Context ctx, NotificationService svs, + RealmManager rm, TokensService tokensService, + TokenLocalSource trs, AlphaWalletService alphaService) { context = ctx; - okHttpClient = client; + ipfsService = ipfsSvs; assetChecked = new ConcurrentHashMap<>(); notificationService = svs; realmManager = rm; alphaWalletService = alphaService; this.tokensService = tokensService; - tokenscriptUtility = new TokenscriptFunction() { }; //no overridden functions + tokenscriptUtility = new TokenscriptFunction() + { + }; //no overridden functions tokenLocalSource = trs; - transactionRepository = trt; assetLoadingLock = new Semaphore(1); eventConnection = new Semaphore(1); //deleteAllEventData(); loadAssetScripts(); } - public TokenLocalSource getTokenLocalSource() { + public TokenLocalSource getTokenLocalSource() + { return tokenLocalSource; } /** * Load all TokenScripts - * + *

* This order has to be observed because it's an expected developer override order. If a script is placed in the /AlphaWallet directory * it is expected to override the one fetched from the repo server. * If a developer clicks on a script intent this script is expected to override the one fetched from the server. @@ -229,7 +226,8 @@ private List checkRealmScriptsForChanges() for (RealmTokenScriptData entry : realmData) { - if (handledHashes.contains(entry.getFileHash())) continue; //already checked - note that if a contract has multiple origins it could have more than one entry + if (handledHashes.contains(entry.getFileHash())) + continue; //already checked - note that if a contract has multiple origins it could have more than one entry //get file TokenScriptFile tsf = new TokenScriptFile(context, entry.getFilePath()); handledHashes.add(entry.getFileHash()); @@ -242,7 +240,7 @@ private List checkRealmScriptsForChanges() handledHashes.add(tsf.calcMD5()); //add the hash of the new file //re-parse script, file hash has changed final TokenDefinition td = parseFile(tsf.getInputStream()); - cacheSignature(tsf) + cacheSignature(tsf) .map(definition -> getOriginContracts(td)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) @@ -296,7 +294,7 @@ private void loadNewFiles(List handledHashes) handledHashes.add(new TokenScriptFile(context, file.getAbsolutePath()).calcMD5()); handleFileLoadError(e, file); } - } ); + }); } private void deleteTokenScriptFromRealm(Realm realm, String fileHash) throws RealmException @@ -396,22 +394,22 @@ private TokenDefinition fileLoadComplete(List originContracts, final String hash = file.calcMD5(); - realm.beginTransaction(); - for (ContractLocator cl : originContracts) - { - String entryKey = getTSDataKey(cl.chainId, cl.address); - RealmTokenScriptData entry = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", entryKey) - .findFirst(); - if (entry != null) continue; // at this point, don't override any existing entry - entry = realm.createObject(RealmTokenScriptData.class, entryKey); - entry.setFileHash(hash); - entry.setFilePath(file.getAbsolutePath()); - entry.setNames(td.getTokenNameList()); - entry.setViewList(td.getViews()); - entry.setHasEvents(hasEvents); - } - realm.commitTransaction(); + realm.executeTransaction(r -> { + for (ContractLocator cl : originContracts) + { + String entryKey = getTSDataKey(cl.chainId, cl.address); + RealmTokenScriptData entry = realm.where(RealmTokenScriptData.class) + .equalTo("instanceKey", entryKey) + .findFirst(); + if (entry != null) continue; // at this point, don't override any existing entry + entry = realm.createObject(RealmTokenScriptData.class, entryKey); + entry.setFileHash(hash); + entry.setFilePath(file.getAbsolutePath()); + entry.setNames(td.getTokenNameList()); + entry.setViewList(td.getViews()); + entry.setHasEvents(hasEvents); + } + }); } catch (Exception e) { @@ -485,11 +483,13 @@ private List buildFileList() //then include the files in the app external directory - these are placed here when there's no file permission files = context.getExternalFilesDir("").listFiles(); - if (files != null) fileList.addAll(Arrays.asList(files)); //now add files in the app's external directory; /Android/data/[app-name]/files. These override internal + if (files != null) + fileList.addAll(Arrays.asList(files)); //now add files in the app's external directory; /Android/data/[app-name]/files. These override internal //finally the files downloaded from the server files = context.getFilesDir().listFiles(); - if (files != null) fileList.addAll(Arrays.asList(files)); //first add files in app internal area - these are downloaded from the server + if (files != null) + fileList.addAll(Arrays.asList(files)); //first add files in app internal area - these are downloaded from the server } catch (Exception e) { @@ -599,7 +599,72 @@ public TokenScriptResult.Attribute fetchAttrResult(ContractAddress origin, Attri if (originToken == null || td == null) return null; //produce result - return tokenscriptUtility.fetchAttrResult(originToken, attr, tokenId, td, this, false).blockingSingle(); + return tokenscriptUtility.fetchAttrResult(originToken, attr, tokenId, td, this, ViewType.VIEW).blockingGet(); + } + + /** + * Refreshes the stored values for a list of attributes + * @param token + * @return + */ + public Single refreshAttributes(Token token, TokenDefinition td, BigInteger tokenId, List attrs) + { + return Single.fromCallable(() -> { + //run through attributes first + for (Attribute attr : attrs) + { + //check if any of these attributes take TokenID + if (attr.usesTokenId()) + { + updateAttributeResult(token, td, attr, tokenId); + } + else + { + updateAttributeResult(token, td, attr, BigInteger.ZERO); + } + } + return true; + }); + } + + /** + * Refreshes the stored values for all Attribute results for all tokenIds + * @param token + * @return + */ + public Single refreshAllAttributes(Token token) + { + TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + if (td == null) return Single.fromCallable(() -> false); + + return Single.fromCallable(() -> { + //run through attributes first + for (Map.Entry attrEntry : td.attributes.entrySet()) + { + //check if any of these attributes take TokenID + if (attrEntry.getValue().usesTokenId()) + { + //run through each tokenId and update + for (BigInteger tokenId : token.getTokenAssets().keySet()) + { + updateAttributeResult(token, td, attrEntry.getValue(), tokenId); + } + } + else + { + updateAttributeResult(token, td, attrEntry.getValue(), BigInteger.ZERO); + } + } + return true; + }); + } + + private void updateAttributeResult(Token token, TokenDefinition td, Attribute attr, BigInteger tokenId) + { + ContractAddress useAddress = new ContractAddress(attr.function); //always use the function attribute's address + tokenscriptUtility.fetchResultFromEthereum(token, useAddress, attr, tokenId, td, this) // Fetch function result from blockchain + .subscribe(txResult -> storeAuxData(getWalletAddr(), txResult)) + .isDisposed(); } public void addLocalRefs(Map refs) @@ -651,7 +716,7 @@ private TokenScriptResult.Attribute getTokenscriptAttr(TokenDefinition td, BigIn } else if (attrtype.event != null) { - result = new TokenScriptResult.Attribute(attrtype.name, attrtype.label, tokenId, "unsupported encoding"); + result = new TokenScriptResult.Attribute(attrtype, tokenId, "unsupported encoding"); } else if (attrtype.function != null) { @@ -663,12 +728,12 @@ else if (attrtype.function != null) else { BigInteger val = tokenId.and(attrtype.bitmask).shiftRight(attrtype.bitshift); - result = new TokenScriptResult.Attribute(attrtype.name, attrtype.label, attrtype.processValue(val), attrtype.getSyntaxVal(attrtype.toString(val))); + result = new TokenScriptResult.Attribute(attrtype, attrtype.processValue(val), attrtype.getSyntaxVal(attrtype.toString(val))); } } catch (Exception e) { - result = new TokenScriptResult.Attribute(attrtype.name, attrtype.label, tokenId, "unsupported encoding"); + result = new TokenScriptResult.Attribute(attrtype, tokenId, "unsupported encoding"); } return result; @@ -687,7 +752,8 @@ public TokenScriptResult.Attribute getAttribute(Token token, BigInteger tokenId, } } - private boolean checkReadPermission() { + private boolean checkReadPermission() + { return context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } @@ -786,7 +852,8 @@ public Single getAssetDefinitionASync(long chainId, final Strin { if (address == null) return Single.fromCallable(TokenDefinition::new); String contractName = address; - if (contractName.equalsIgnoreCase(tokensService.getCurrentAddress())) contractName = "ethereum"; + if (contractName.equalsIgnoreCase(tokensService.getCurrentAddress())) + contractName = "ethereum"; // hold until asset definitions have finished loading waitForAssets(); @@ -879,7 +946,7 @@ public String getIssuerName(Token token) private void loadScriptFromServer(String correctedAddress) { //first check the last time we tried this session - if (assetChecked.get(correctedAddress) == null || (System.currentTimeMillis() > (assetChecked.get(correctedAddress) + 1000L*60L*60L))) + if (assetChecked.get(correctedAddress) == null || (System.currentTimeMillis() > (assetChecked.get(correctedAddress) + 1000L * 60L * 60L))) { fetchXMLFromServer(correctedAddress) .flatMap(this::cacheSignature) @@ -903,7 +970,8 @@ private void onError(Throwable throwable) private TokenDefinition parseFile(InputStream xmlInputStream) throws Exception { Locale locale; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + { locale = context.getResources().getConfiguration().getLocales().get(0); } else @@ -917,6 +985,10 @@ private TokenDefinition parseFile(InputStream xmlInputStream) throws Exception private Single handleNewTSFile(File newFile) { + if (!newFile.exists()) + { + return Single.fromCallable(TokenDefinition::new); + } //1. check validity & check for origin tokens //2. check for existing and check if this is a debug file or script from server //3. update signature data @@ -928,7 +1000,7 @@ private Single handleNewTSFile(File newFile) final TokenDefinition td = parseFile(tsf.getInputStream()); List originContracts = getOriginContracts(td); //remove all old definitions & certificates - deleteScriptEntriesFromRealm(originContracts, isDebugOverride); + deleteScriptEntriesFromRealm(originContracts, isDebugOverride, tsf.calcMD5()); cachedDefinition = null; return cacheSignature(tsf) .map(contracts -> fileLoadComplete(originContracts, tsf, td)); @@ -941,7 +1013,7 @@ private Single handleNewTSFile(File newFile) return Single.fromCallable(TokenDefinition::new); } - private void deleteScriptEntriesFromRealm(List origins, boolean isDebug) + private void deleteScriptEntriesFromRealm(List origins, boolean isDebug, String newFileHash) { try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { @@ -958,7 +1030,8 @@ private void deleteScriptEntriesFromRealm(List origins, boolean RealmCertificateData realmCert = r.where(RealmCertificateData.class) .equalTo("instanceKey", realmData.getFileHash()) .findFirst(); - if (realmCert != null) realmCert.deleteFromRealm(); + if (realmCert != null && !realmData.getFileHash().equals(newFileHash)) + realmCert.deleteFromRealm(); //don't delete cert if new cert will overwrite it deleteEventDataForScript(realmData); realmData.deleteFromRealm(); } @@ -973,9 +1046,10 @@ public Single fetchTokenScriptFromContract(Token token, MutableLiveData { if (!TextUtils.isEmpty(uri)) updateFlag.postValue(true); - return uri; }) + return uri; + }) .map(uri -> downloadScript(uri, 0)) - .map(xmlBody -> storeFile(token.tokenInfo.address, xmlBody)); + .map(dlResponse -> storeFile(token.tokenInfo.address, dlResponse)); } private Single tryServerIfRequired(File contractScript, String address) @@ -1018,17 +1092,18 @@ private Single fetchXMLFromServer(String address) result = defaultReturn; } - if (assetChecked.get(address) != null && (System.currentTimeMillis() > (assetChecked.get(address) + 1000L*60L*60L))) return result; + if (assetChecked.get(address) != null && (System.currentTimeMillis() > (assetChecked.get(address) + 1000L * 60L * 60L))) + return result; String sb = TOKENSCRIPT_REPO_SERVER + TOKENSCRIPT_CURRENT_SCHEMA + "/" + address; - String xmlBody = downloadScript(sb, fileTime); - if (!TextUtils.isEmpty(xmlBody)) + Pair downloadResponse = downloadScript(sb, fileTime); + if (!TextUtils.isEmpty(downloadResponse.first)) { - result = storeFile(address, xmlBody); + result = storeFile(address, downloadResponse); } assetChecked.put(address, System.currentTimeMillis()); @@ -1037,57 +1112,53 @@ private Single fetchXMLFromServer(String address) }); } - private String downloadScript(String Uri, long currentFileTime) throws PackageManager.NameNotFoundException + private Pair downloadScript(String Uri, long currentFileTime) { - if (TextUtils.isEmpty(Uri)) return ""; - SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - String dateFormat = format.format(new Date(currentFileTime)); + boolean isIPFS = Utils.isIPFS(Uri); - //convert uri if using IPFS: - Uri = Utils.parseIPFS(Uri); - - //prepare Android headers - PackageManager manager = context.getPackageManager(); - PackageInfo info = manager.getPackageInfo( - context.getPackageName(), 0); - String appVersion = info.versionName; - String OSVersion = String.valueOf(Build.VERSION.RELEASE); - - Request.Builder bld = new Request.Builder() - .url(Uri) - .get(); - - if (!Uri.toLowerCase().contains("ipfs")) - { - bld.addHeader("Accept", "text/xml; charset=UTF-8") - .addHeader("X-Client-Name", "AlphaWallet") - .addHeader("X-Client-Version", appVersion) - .addHeader("X-Platform-Name", "Android") - .addHeader("X-Platform-Version", OSVersion) - .addHeader("If-Modified-Since", dateFormat); - } - - Request request = bld.build(); - - try (okhttp3.Response response = okHttpClient.newCall(request) - .execute()) + try { - switch (response.code()) + QueryResponse response = ipfsService.performIO(Uri, getHeaders(currentFileTime)); + switch (response.code) { default: case HttpURLConnection.HTTP_NOT_MODIFIED: break; case HttpURLConnection.HTTP_OK: - return response.body().string(); + return new Pair<>(response.body, isIPFS); } } catch (Exception e) { - Timber.e(e); + if (!TextUtils.isEmpty(Uri)) //throws on empty, which is expected + { + Timber.w(e); + } } - return ""; + return new Pair<>("", false); + } + + private String[] getHeaders(long currentFileTime) throws PackageManager.NameNotFoundException + { + SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + String dateFormat = format.format(new Date(currentFileTime)); + + PackageManager manager = context.getPackageManager(); + PackageInfo info = manager.getPackageInfo( + context.getPackageName(), 0); + String appVersion = info.versionName; + String OSVersion = String.valueOf(Build.VERSION.RELEASE); + + return new String[] { + "Accept", "text/xml; charset=UTF-8", + "X-Client-Name", "AlphaWallet", + "X-Client-Version", appVersion, + "X-Platform-Name", "Android", + "X-Platform-Version", OSVersion, + "If-Modified-Since", dateFormat + }; } private boolean definitionIsOutOfDate(TokenDefinition td) @@ -1120,12 +1191,14 @@ private void removeFile(String filename) /** * Only used for loading bundled TokenScripts + * * @param asset * @return */ private boolean addContractAssets(String asset) { - try (InputStream input = context.getResources().getAssets().open(asset)) { + try (InputStream input = context.getResources().getAssets().open(asset)) + { TokenDefinition token = parseFile(input); TokenScriptFile tsf = new TokenScriptFile(context, asset); ContractInfo holdingContracts = token.contracts.get(token.holdingToken); @@ -1149,7 +1222,9 @@ private boolean addContractAssets(String asset) } return true; } - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } return false; @@ -1174,7 +1249,7 @@ private void updateRealmForBundledScript(long chainId, String address, String as { try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { - realm.executeTransactionAsync(r -> { + realm.executeTransaction(r -> { String entryKey = getTSDataKey(chainId, address); RealmTokenScriptData entry = r.where(RealmTokenScriptData.class) .equalTo("instanceKey", entryKey) @@ -1193,7 +1268,8 @@ private void updateRealmForBundledScript(long chainId, String address, String as public TokenDefinition getTokenDefinition(File file) { - try (FileInputStream input = new FileInputStream(file)) { + try (FileInputStream input = new FileInputStream(file)) + { return parseFile(input); } catch (Exception e) @@ -1247,7 +1323,8 @@ private void checkAddToEventList(EventDefinition ev) public void stopEventListener() { if (eventListener != null && !eventListener.isDisposed()) eventListener.dispose(); - if (checkEventDisposable != null && !checkEventDisposable.isDisposed()) checkEventDisposable.dispose(); + if (checkEventDisposable != null && !checkEventDisposable.isDisposed()) + checkEventDisposable.dispose(); } public void startEventListener() @@ -1255,7 +1332,7 @@ public void startEventListener() if (assetLoadingLock.availablePermits() == 0) return; if (eventListener != null && !eventListener.isDisposed()) eventListener.dispose(); - eventListener = Observable.interval(0, CHECK_TX_LOGS_INTERVAL, TimeUnit.SECONDS) + eventListener = Observable.interval(0, CHECK_TX_LOGS_INTERVAL, TimeUnit.SECONDS) .doOnNext(l -> { checkEvents() .subscribeOn(Schedulers.io()) @@ -1282,7 +1359,8 @@ private void getEvent(EventDefinition ev) { EthFilter filter = getEventFilter(ev); if (filter == null) return; - if (BuildConfig.DEBUG) eventConnection.acquire(); //prevent overlapping event calls while debugging + if (BuildConfig.DEBUG) + eventConnection.acquire(); //prevent overlapping event calls while debugging final String walletAddress = tokensService.getCurrentAddress(); Web3j web3j = getWeb3jService(ev.getEventChainId()); @@ -1340,7 +1418,7 @@ private EthFilter getEventFilter(EventDefinition ev) throws Exception private String processLogs(EventDefinition ev, List logs, String walletAddress) { - if (logs.size() == 0) return ""; //early return + if (logs == null || logs.size() == 0) return ""; //early return long chainId = ev.contract.addresses.keySet().iterator().next(); Web3j web3j = getWeb3jService(chainId); @@ -1350,11 +1428,11 @@ private String processLogs(EventDefinition ev, List logs, Stri for (int i = index; i >= 0; i--) { - EthLog.LogResult ethLog = logs.get(i); - String txHash = ((Log)ethLog.get()).getTransactionHash(); + EthLog.LogResult ethLog = logs.get(i); + String txHash = ((Log) ethLog.get()).getTransactionHash(); if (TextUtils.isEmpty(firstTxHash)) firstTxHash = txHash; String selectVal = EventUtils.getSelectVal(ev, ethLog); - BigInteger blockNumber = ((Log)ethLog.get()).getBlockNumber(); + BigInteger blockNumber = ((Log) ethLog.get()).getBlockNumber(); if (blockNumber.compareTo(ev.readBlock) > 0) { @@ -1368,7 +1446,7 @@ private String processLogs(EventDefinition ev, List logs, Stri } else { - EthBlock txBlock = EventUtils.getBlockDetails(((Log)ethLog.get()).getBlockHash(), web3j).blockingGet(); + EthBlock txBlock = EventUtils.getBlockDetails(((Log) ethLog.get()).getBlockHash(), web3j).blockingGet(); long blockTime = txBlock.getBlock().getTimestamp().longValue(); storeActivityValue(walletAddress, ev, ethLog, blockTime, ev.activityName); @@ -1393,7 +1471,8 @@ private void storeLatestEventBlockTime(String walletAddress, EventDefinition ev, RealmAuxData realmToken = r.where(RealmAuxData.class) .equalTo("instanceKey", databaseKey) .findFirst(); - if (realmToken == null) realmToken = r.createObject(RealmAuxData.class, databaseKey); + if (realmToken == null) + realmToken = r.createObject(RealmAuxData.class, databaseKey); realmToken.setResultTime(System.currentTimeMillis()); realmToken.setResult(ev.readBlock.toString(16)); realmToken.setFunctionId(eventName); @@ -1432,7 +1511,8 @@ private void storeAuxData(String walletAddress, String databaseKey, BigInteger t RealmAuxData realmToken = r.where(RealmAuxData.class) .equalTo("instanceKey", databaseKey) .findFirst(); - if (realmToken == null) realmToken = r.createObject(RealmAuxData.class, databaseKey); + if (realmToken == null) + realmToken = r.createObject(RealmAuxData.class, databaseKey); realmToken.setResultTime(blockTime); realmToken.setResult(eventData); realmToken.setFunctionId(activityName); @@ -1448,8 +1528,8 @@ private void storeAuxData(String walletAddress, String databaseKey, BigInteger t } } - private void storeEventValue(String walletAddress, EventDefinition ev, EthLog.LogResult log, Attribute attr, - String selectVal) + private void storeEventValue (String walletAddress, EventDefinition ev, EthLog.LogResult log, Attribute attr, + String selectVal) { //store result BigInteger tokenId = EventUtils.getTokenId(ev, log); @@ -1459,7 +1539,7 @@ private void storeEventValue(String walletAddress, EventDefinition ev, EthLog.Lo TransactionResult txResult = getFunctionResult(eventContractAddress, attr, tokenId); txResult.result = attr.getSyntaxVal(selectVal); - long blockNumber = ((Log)log.get()).getBlockNumber().longValue(); + long blockNumber = ((Log) log.get()).getBlockNumber().longValue(); //Update the entry for the attribute if required if (txResult.resultTime == 0 || blockNumber >= txResult.resultTime) @@ -1474,7 +1554,7 @@ private boolean allowableExtension(File file) int index = file.getName().lastIndexOf("."); if (index >= 0) { - String extension = file.getName().substring(index+1); + String extension = file.getName().substring(index + 1); switch (extension) { case "xml": @@ -1512,6 +1592,7 @@ private boolean isAddress(File file) /** * This is used to retrieve the file from the secure area in order to check the date. * Note: it only finds files previously downloaded from the server + * * @param contractAddress * @return */ @@ -1543,7 +1624,7 @@ private List getScriptsInSecureZone() .filter(this::allowableExtension) .filter(File::canRead) .filter(this::isAddress) - .forEach(file -> checkScripts.add(getFileName(file)) ).isDisposed(); + .forEach(file -> checkScripts.add(getFileName(file))).isDisposed(); return checkScripts; } @@ -1586,15 +1667,18 @@ private void storeCertificateData(String hash, XMLDsigDescriptor sig) throws Rea { try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { - realm.executeTransactionAsync(r -> { + realm.executeTransaction(r -> { //if signature present, then just update RealmCertificateData realmData = r.where(RealmCertificateData.class) .equalTo("instanceKey", hash) .findFirst(); if (realmData == null) + { realmData = r.createObject(RealmCertificateData.class, hash); + } realmData.setFromSig(sig); + r.insertOrUpdate(realmData); }); } } @@ -1628,16 +1712,30 @@ private XMLDsigDescriptor getCertificateFromRealm(String hash) return sig; } + private XMLDsigDescriptor IPFSSigDescriptor() + { + XMLDsigDescriptor sig = new XMLDsigDescriptor(); + sig.issuer = EIP5169_ISSUER; + sig.certificateName = EIP5169_CERTIFIER; + sig.keyName = EIP5169_KEY_OWNER; + sig.keyType = "ECDSA"; + sig.result = "Pass"; + sig.subject = ""; + sig.type = SigReturnType.SIGNATURE_PASS; + return sig; + } + /** * Use internal directory to store contracts fetched from the server + * * @param address * @param result * @return * @throws */ - private File storeFile(String address, String result) throws IOException + private File storeFile(String address, Pair result) throws IOException { - if (result == null || result.length() < 10) return new File(""); + if (result.first == null || result.first.length() < 10) return new File(""); String fName = address + ".xml"; @@ -1646,10 +1744,19 @@ private File storeFile(String address, String result) throws IOException FileOutputStream fos = new FileOutputStream(file); OutputStream os = new BufferedOutputStream(fos); - os.write(result.getBytes()); + os.write(result.first.getBytes()); fos.flush(); os.close(); fos.close(); + + //handle signature for IPFS + if (result.second) + { + TokenScriptFile tsf = new TokenScriptFile(context, file.getAbsolutePath()); + String hash = tsf.calcMD5(); + storeCertificateData(hash, IPFSSigDescriptor()); + } + return file; } @@ -1738,20 +1845,48 @@ public Map getTokenFunctionMap(long chainId, String contractAd } } + public List getLocalAttributes(TokenDefinition td, Map> availableActions) + { + List attrs = new ArrayList<>(); + if (td != null) + { + for (Map.Entry> availableAction : availableActions.entrySet()) + { + attrs.addAll(getLocalAttributes(td, availableAction.getValue())); + } + } + + return attrs; + } + + private List getLocalAttributes(TokenDefinition td, List actions) + { + List attrs = new ArrayList<>(); + for (Map.Entry action : td.getActions().entrySet()) + { + if (!actions.contains(action.getKey())) + { + continue; + } + attrs.addAll(action.getValue().attributes.values()); + } + + return attrs; + } + /** * Build a map of all available tokenIds to a list of available functions for that tokenId * * @param token * @return map of unique tokenIds to lists of allowed functions for that ID - note that we allow the function to be displayed if it has a denial message */ - public Single>> fetchFunctionMap(Token token) + public Single>> fetchFunctionMap(Token token, @NotNull List tokenIds) { return Single.fromCallable(() -> { Map> validActions = new HashMap<>(); TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress()); if (td != null) { - List tokenIds = token.getUniqueTokenIds(); Map actions = td.getActions(); //first gather all attrs required - do this so if there's multiple actions using the same attribute for a tokenId we aren't fetching the value repeatedly List requiredAttrNames = getRequiredAttributeNames(actions, td); @@ -1766,7 +1901,8 @@ public Single>> fetchFunctionMap(Token token) TSSelection selection = action.exclude != null ? td.getSelection(action.exclude) : null; if (selection == null) { - if (!validActions.containsKey(tokenId)) validActions.put(tokenId, new ArrayList<>()); + if (!validActions.containsKey(tokenId)) + validActions.put(tokenId, new ArrayList<>()); validActions.get(tokenId).add(actionName); } else @@ -1780,7 +1916,8 @@ public Single>> fetchFunctionMap(Token token) boolean exclude = EvaluateSelection.evaluate(selection.head, idAttrResults); if (!exclude || selection.denialMessage != null) { - if (!validActions.containsKey(tokenId)) validActions.put(tokenId, new ArrayList<>()); + if (!validActions.containsKey(tokenId)) + validActions.put(tokenId, new ArrayList<>()); validActions.get(tokenId).add(actionName); } } @@ -1820,7 +1957,7 @@ public String checkFunctionDenied(Token token, String actionName, List getAttributeResultsForTokenIds(Map> attrResults, List requiredAttributeNames, BigInteger tokenId) + private Map getAttributeResultsForTokenIds(Map> attrResults, List requiredAttributeNames, + BigInteger tokenId) { Map results = new HashMap<>(); if (!attrResults.containsKey(tokenId)) return results; //check values @@ -1859,7 +1997,7 @@ private Map> getRequiredAtt { Attribute attr = td.attributes.get(attrName); if (attr == null) continue; - TokenScriptResult.Attribute attrResult = tokenscriptUtility.fetchAttrResult(token, attr, tokenId, td, this, false).blockingSingle(); + TokenScriptResult.Attribute attrResult = tokenscriptUtility.fetchAttrResult(token, attr, tokenId, td, this, ViewType.VIEW).blockingGet(); if (attrResult != null) { Map tokenIdMap = resultSet.get(tokenId); @@ -1917,6 +2055,7 @@ private FileObserver startFileListener(String path) FileObserver observer = new FileObserver(path) { private final String listenerPath = path; + @Override public void onEvent(int event, @Nullable String file) { @@ -1945,7 +2084,8 @@ public void onEvent(int event, @Nullable String file) } catch (Exception e) { - if (homeMessenger != null) homeMessenger.tokenScriptError(e.getMessage()); + if (homeMessenger != null) + homeMessenger.tokenScriptError(e.getMessage()); } break; default: @@ -1990,7 +2130,7 @@ public Single getSignatureData(long chainId, String contractA { String hash = tsf.calcMD5(); XMLDsigDescriptor sig = getCertificateFromRealm(hash); - if (sig == null || (sig.result != null && sig.result.equals("fail"))) + if (sig == null || (sig.result != null && sig.result.equalsIgnoreCase("fail"))) { sig = alphaWalletService.checkTokenScriptSignature(tsf); tsf.determineSignatureType(sig); @@ -2003,58 +2143,6 @@ public Single getSignatureData(long chainId, String contractA }); } - private void checkCorrectInterface(Token token, String contractInterface) - { - ContractType cType; - switch (contractInterface.toLowerCase()) - { - case "erc875": - cType = ContractType.ERC875; - if (token.isERC875()) return; - break; - case "erc20": - cType = ContractType.ERC20; - break; - // note: ERC721 and ERC721Ticket are contracts with different interfaces which are handled in different ways but we describe them - // as the same within the tokenscript. - case "erc721": - if (token.isERC721() || token.isERC721Ticket()) return; - cType = ContractType.ERC721; - break; - case "erc721ticket": - if (token.isERC721() || token.isERC721Ticket()) return; - cType = ContractType.ERC721_TICKET; - break; - case "ethereum": - cType = ContractType.ETHEREUM; - break; - default: - cType = ContractType.OTHER; - break; - } - - if (cType == ContractType.OTHER) return; - if (cType == token.getInterfaceSpec()) return; - - //contract mismatch, re-assign - //first delete from database - tokenLocalSource.deleteRealmToken(token.tokenInfo.chainId, new Wallet(token.getWallet()), token.tokenInfo.address); - - //now store into database - //TODO: if erc20 refresh all values - TokenFactory tf = new TokenFactory(); - - Token newToken = tf.createToken(token.tokenInfo, BigDecimal.ZERO, null, 0, cType, token.getNetworkName(), 0); - newToken.setTokenWallet(token.getWallet()); - newToken.walletUIUpdateRequired = true; - newToken.updateBlancaTime = 0; - - tokenLocalSource.saveToken(new Wallet(token.getWallet()), newToken) - .subscribeOn(Schedulers.io()) - .subscribe() - .isDisposed(); - } - //Database functions private String functionKey(ContractAddress cAddr, BigInteger tokenId, String attrId) { @@ -2091,13 +2179,14 @@ public TransactionResult getFunctionResult(ContractAddress contract, Attribute a @Override public TransactionResult storeAuxData(String walletAddress, TransactionResult tResult) { - if (tokensService.getCurrentAddress() == null || !Utils.isAddressValid(tokensService.getCurrentAddress())) return tResult; + if (tokensService.getCurrentAddress() == null || !Utils.isAddressValid(tokensService.getCurrentAddress())) + return tResult; if (tResult.result == null || tResult.resultTime < 0) return tResult; try (Realm realm = realmManager.getRealmInstance(walletAddress)) { ContractAddress cAddr = new ContractAddress(tResult.contractChainId, tResult.contractAddress); String databaseKey = functionKey(cAddr, tResult.tokenId, tResult.attrId); - realm.executeTransactionAsync(r -> { + realm.executeTransaction(r -> { RealmAuxData realmToken = r.where(RealmAuxData.class) .equalTo("instanceKey", databaseKey) .equalTo("chainId", tResult.contractChainId) @@ -2221,20 +2310,25 @@ private void createAuxData(Realm realm, TransactionResult tResult, String dataBa //private Token - private void addOpenSeaAttributes(StringBuilder attrs, Token erc721Token, BigInteger tokenId) + private void addOpenSeaAttributes(StringBuilder attrs, Token token, BigInteger tokenId) { - NFTAsset tokenAsset = erc721Token.getAssetForToken(tokenId.toString()); - if(tokenAsset == null) return; + NFTAsset tokenAsset = token.getAssetForToken(tokenId.toString()); + if (tokenAsset == null) return; try { //add all asset IDs - if (tokenAsset.getBackgroundColor() != null) TokenScriptResult.addPair(attrs, "background_colour", URLEncoder.encode(tokenAsset.getBackgroundColor(), "utf-8")); - if (tokenAsset.getThumbnail() != null) TokenScriptResult.addPair(attrs, "image_preview_url", URLEncoder.encode(tokenAsset.getThumbnail(), "utf-8")); - if (tokenAsset.getDescription() != null) TokenScriptResult.addPair(attrs, "description", URLEncoder.encode(tokenAsset.getDescription(), "utf-8")); - if (tokenAsset.getExternalLink() != null) TokenScriptResult.addPair(attrs, "external_link", URLEncoder.encode(tokenAsset.getExternalLink(), "utf-8")); + if (tokenAsset.getBackgroundColor() != null) + TokenScriptResult.addPair(attrs, "background_colour", URLEncoder.encode(tokenAsset.getBackgroundColor(), "utf-8")); + if (tokenAsset.getThumbnail() != null) + TokenScriptResult.addPair(attrs, "image_preview_url", URLEncoder.encode(tokenAsset.getThumbnail(), "utf-8")); + if (tokenAsset.getDescription() != null) + TokenScriptResult.addPair(attrs, "description", URLEncoder.encode(tokenAsset.getDescription(), "utf-8")); + if (tokenAsset.getExternalLink() != null) + TokenScriptResult.addPair(attrs, "external_link", URLEncoder.encode(tokenAsset.getExternalLink(), "utf-8")); //if (tokenAsset.getTraits() != null) TokenScriptResult.addPair(attrs, "traits", tokenAsset.getTraits()); - if (tokenAsset.getName() != null) TokenScriptResult.addPair(attrs, "name", URLEncoder.encode(tokenAsset.getName(), "utf-8")); + if (tokenAsset.getName() != null) + TokenScriptResult.addPair(attrs, "metadata_name", URLEncoder.encode(tokenAsset.getName(), "utf-8")); } catch (UnsupportedEncodingException e) { @@ -2247,21 +2341,21 @@ public StringBuilder getTokenAttrs(Token token, BigInteger tokenId, int count) StringBuilder attrs = new StringBuilder(); TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); - String name = token.getTokenTitle(); + String label = token.getTokenTitle(); if (definition != null && definition.getTokenName(1) != null) { - name = definition.getTokenName(1); + label = definition.getTokenName(1); } - TokenScriptResult.addPair(attrs, "name", name); - TokenScriptResult.addPair(attrs, "label", name); + TokenScriptResult.addPair(attrs, "name", token.tokenInfo.name); + TokenScriptResult.addPair(attrs, "label", label); TokenScriptResult.addPair(attrs, "symbol", token.getSymbol()); - TokenScriptResult.addPair(attrs, "_count", String.valueOf(count)); + TokenScriptResult.addPair(attrs, "_count", BigInteger.valueOf(count)); TokenScriptResult.addPair(attrs, "contractAddress", token.tokenInfo.address); - TokenScriptResult.addPair(attrs, "chainId", String.valueOf(token.tokenInfo.chainId)); + TokenScriptResult.addPair(attrs, "chainId", BigInteger.valueOf(token.tokenInfo.chainId)); TokenScriptResult.addPair(attrs, "tokenId", tokenId); TokenScriptResult.addPair(attrs, "ownerAddress", token.getWallet()); - if(token instanceof ERC721Token) + if (token.isNonFungible()) { addOpenSeaAttributes(attrs, token, tokenId); } @@ -2276,6 +2370,7 @@ public StringBuilder getTokenAttrs(Token token, BigInteger tokenId, int count) /** * Get all the magic values - eg native crypto balances for all chains + * * @return */ public String getMagicValuesForInjection(long chainId) throws Exception @@ -2303,11 +2398,12 @@ public void clearResultMap() tokenscriptUtility.clearParseMaps(); } - public Observable resolveAttrs(Token token, BigInteger tokenId, List extraAttrs, boolean itemView) + public Observable resolveAttrs(Token token, BigInteger tokenId, List extraAttrs, ViewType itemView) { TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); ContractAddress cAddr = new ContractAddress(token.tokenInfo.chainId, token.tokenInfo.address); - if (definition == null) return Observable.fromCallable(() -> new TokenScriptResult.Attribute("RAttrs", "", BigInteger.ZERO, "")); + if (definition == null) + return Observable.fromCallable(() -> new TokenScriptResult.Attribute("RAttrs", "", BigInteger.ZERO, "")); definition.context = new TokenscriptContext(); definition.context.cAddr = cAddr; @@ -2316,16 +2412,24 @@ public Observable resolveAttrs(Token token, BigInte List attrList = new ArrayList<>(definition.attributes.values()); if (extraAttrs != null) attrList.addAll(extraAttrs); - tokenscriptUtility.buildAttrMap(attrList); + return resolveAttrs(token, tokenId, definition, attrList, itemView); + } + private Observable resolveAttrs(Token token, BigInteger tokenId, TokenDefinition td, List attrList, ViewType itemView) + { + tokenscriptUtility.buildAttrMap(attrList); return Observable.fromIterable(attrList) .flatMap(attr -> tokenscriptUtility.fetchAttrResult(token, attr, tokenId, - definition, this, itemView)); + td, this, itemView).toObservable()); } public Observable resolveAttrs(Token token, List tokenIds, List extraAttrs) { TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + if (definition == null) + { + return Observable.fromCallable(() -> new TokenScriptResult.Attribute("", "", BigInteger.ZERO, "")); + } //pre-fill tokenIds for (Attribute attrType : definition.attributes.values()) { @@ -2334,7 +2438,7 @@ public Observable resolveAttrs(Token token, List tokenIds) @@ -2475,7 +2579,7 @@ public Single> getAllTokenDefinitions(boolean refresh) catch (Exception e) { TokenScriptFile tsf = new TokenScriptFile(context, file.getAbsolutePath()); - ContractInfo contractInfo = new ContractInfo("Contract Type",new HashMap<>()); + ContractInfo contractInfo = new ContractInfo("Contract Type", new HashMap<>()); StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); @@ -2490,7 +2594,8 @@ public Single> getAllTokenDefinitions(boolean refresh) public Single checkServerForScript(Token token, MutableLiveData updateFlag) { TokenScriptFile tf = getTokenScriptFile(token.tokenInfo.chainId, token.getAddress()); - if ((tf != null && !TextUtils.isEmpty(tf.getName())) && !isInSecureZone(tf)) return Single.fromCallable(TokenDefinition::new); //early return for debug script check + if ((tf != null && !TextUtils.isEmpty(tf.getName())) && !isInSecureZone(tf)) + return Single.fromCallable(TokenDefinition::new); //early return for debug script check //try the contractURI, then server return fetchTokenScriptFromContract(token, updateFlag) @@ -2601,7 +2706,7 @@ public Single fetchViewHeight(long chainId, String address) if (hash.equals(realmToken.getResult())) { //can use this height - return (int)realmToken.getResultTime(); + return (int) realmToken.getResultTime(); } } catch (Exception e) diff --git a/app/src/main/java/com/alphawallet/app/service/GasService.java b/app/src/main/java/com/alphawallet/app/service/GasService.java index 5e35c268bb..b10e17b829 100644 --- a/app/src/main/java/com/alphawallet/app/service/GasService.java +++ b/app/src/main/java/com/alphawallet/app/service/GasService.java @@ -26,6 +26,9 @@ import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; +import com.alphawallet.app.repository.HttpServiceHelper; +import com.alphawallet.app.repository.KeyProvider; +import com.alphawallet.app.repository.KeyProviderFactory; import com.alphawallet.app.repository.entity.Realm1559Gas; import com.alphawallet.app.repository.entity.RealmGasSpread; import com.alphawallet.app.web3.entity.Web3Transaction; @@ -85,13 +88,6 @@ public class GasService implements ContractGasProvider @Nullable private Disposable gasFetchDisposable; - static { - System.loadLibrary("keys"); - } - - public static native String getEtherscanKey(); - public static native String getPolygonScanKey(); - public GasService(EthereumNetworkRepositoryType networkRepository, OkHttpClient httpClient, RealmManager realm) { this.networkRepository = networkRepository; @@ -101,8 +97,9 @@ public GasService(EthereumNetworkRepositoryType networkRepository, OkHttpClient this.currentChainId = MAINNET_ID; web3j = null; - ETHERSCAN_API_KEY = "&apikey=" + getEtherscanKey(); - POLYGONSCAN_API_KEY = "&apikey=" + getPolygonScanKey(); + KeyProvider keyProvider = KeyProviderFactory.get(); + ETHERSCAN_API_KEY = "&apikey=" + keyProvider.getEtherscanKey(); + POLYGONSCAN_API_KEY = "&apikey=" + keyProvider.getPolygonScanKey(); keyFail = false; } @@ -155,7 +152,7 @@ private void fetchCurrentGasPrice() .map(result -> updateEIP1559Realm(result, currentChainId)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(r -> Timber.d(r ? "Updated Fees" : "Fail to update fees"), this::handleError).isDisposed(); + .subscribe(r -> { if (!r) Timber.d("Fail to update fees"); }, this::handleError).isDisposed(); } private Single useNodeFallback(Boolean updated) @@ -283,7 +280,7 @@ private Single updateEtherscanGasPrices(String gasOracleAPI) } catch (Exception e) { - Timber.e(e); + Timber.w(e); } return update; @@ -316,7 +313,7 @@ private void updateRealm(final GasPriceSpread oracleResult, final long chainId) private boolean updateEIP1559Realm(final Map result, final long chainId) { - boolean hasError = false; + boolean succeeded = true; try (Realm realm = realmManager.getRealmInstance(TICKER_DB)) { realm.executeTransaction(r -> { @@ -334,10 +331,10 @@ private boolean updateEIP1559Realm(final Map re } catch (Exception e) { - hasError = true; + succeeded = false; } - return hasError; + return succeeded; } public Single calculateGasEstimate(byte[] transactionBytes, long chainId, String toAddress, @@ -458,6 +455,7 @@ public static BigInteger getDefaultGasLimit(Token token, Web3Transaction tx) case ERC721_LEGACY: case ERC721_TICKET: case ERC721_UNDETERMINED: + case ERC721_ENUMERABLE: return new BigInteger(DEFAULT_GAS_LIMIT_FOR_NONFUNGIBLE_TOKENS); default: //unknown @@ -474,11 +472,16 @@ public Single getChainFeeHistory(int blockCount, String lastBlock, S RequestBody requestBody = RequestBody.create(requestJSON, HttpService.JSON_MEDIA_TYPE); NetworkInfo info = networkRepository.getNetworkByChain(currentChainId); + final Request.Builder rqBuilder = new Request.Builder() + .url(info.rpcServerUrl) + .post(requestBody); + + HttpServiceHelper.addRequiredCredentials(info.chainId, rqBuilder, KeyProviderFactory.get().getKlaytnKey(), + KeyProviderFactory.get().getInfuraSecret(), EthereumNetworkBase.usesProductionKey, EthereumNetworkBase.isInfura(info.rpcServerUrl)); + return Single.fromCallable(() -> { - Request request = new Request.Builder() - .url(info.rpcServerUrl) - .post(requestBody) - .build(); + Request request = rqBuilder.build(); + try (Response response = httpClient.newCall(request).execute()) { if (response.code() / 200 == 1) @@ -489,11 +492,11 @@ public Single getChainFeeHistory(int blockCount, String lastBlock, S } catch (org.json.JSONException j) { - Timber.d("Note: " + info.getShortName() + " does not appear to have EIP1559 support"); + Timber.e("Note: " + info.getShortName() + " does not appear to have EIP1559 support"); } catch (Exception e) { - Timber.w(e); + Timber.e(e); } return new FeeHistory(); diff --git a/app/src/main/java/com/alphawallet/app/service/IPFSService.java b/app/src/main/java/com/alphawallet/app/service/IPFSService.java new file mode 100644 index 0000000000..36b82f05c8 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/service/IPFSService.java @@ -0,0 +1,135 @@ +package com.alphawallet.app.service; + +import android.text.TextUtils; + +import com.alphawallet.app.entity.QueryResponse; +import com.alphawallet.app.entity.tokenscript.TestScript; +import com.alphawallet.app.util.Utils; + +import java.io.IOException; +import java.net.SocketTimeoutException; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import timber.log.Timber; + +/** + * Created by JB on 3/11/2022. + */ +public class IPFSService implements IPFSServiceType +{ + private final OkHttpClient client; + + public IPFSService(OkHttpClient okHttpClient) + { + this.client = okHttpClient; + } + + public String getContent(String url) + { + try + { + QueryResponse response = performIO(url, null); + + if (response.isSuccessful()) + { + return response.body; + } + else + { + return ""; + } + } + catch (Exception e) + { + Timber.w(e); + return ""; + } + } + + public QueryResponse performIO(String url, String[] headers) throws IOException + { + url = url.trim(); + if (!Utils.isValidUrl(url)) + { + throw new IOException("URL not valid"); + } + + if (Utils.isIPFS(url)) //note that URL might contain IPFS, but not pass 'isValidUrl' + { + return getFromIPFS(url); + } + else + { + return get(url, headers); + } + } + + private QueryResponse get(String url, String[] headers) throws IOException + { + Request.Builder bld = new Request.Builder() + .url(url) + .get(); + + if (headers != null) addHeaders(bld, headers); + + Response response = client.newCall(bld.build()).execute(); + return new QueryResponse(response.code(), response.body().string()); + } + + private QueryResponse getFromIPFS(String url) throws IOException + { + if (isTestCode(url)) return loadTestCode(); + + //try Infura first + String tryIPFS = Utils.resolveIPFS(url, Utils.IPFS_IO_RESOLVER); + //attempt to load content + QueryResponse r; + try + { + r = get(tryIPFS, null); + } + catch (SocketTimeoutException e) + { + //timeout, try second node. Any other failure simply throw back to calling function + tryIPFS = Utils.resolveIPFS(url, Utils.IPFS_INFURA_RESOLVER); + r = get(tryIPFS, null); //if this throws it will be picked up by calling function + } + + return r; + } + + private void addHeaders(Request.Builder bld, String[] headers) throws IOException + { + if (headers.length % 2 != 0) + throw new IOException("Headers must be even value: [{name, value}, {...}]"); + + String name = null; + + for (String header : headers) + { + if (name == null) + { + name = header; + } + else + { + bld.addHeader(name, header); + name = null; + } + } + } + + //For testing + private boolean isTestCode(String url) + { + return (!TextUtils.isEmpty(url) && url.endsWith("QmXXLFBeSjXAwAhbo1344wJSjLgoUrfUK9LE57oVubaRRp")); + } + + private QueryResponse loadTestCode() + { + //restore the TokenScript for the certificate test + return new QueryResponse(200, TestScript.testScriptXXLF); + } +} diff --git a/app/src/main/java/com/alphawallet/app/service/IPFSServiceType.java b/app/src/main/java/com/alphawallet/app/service/IPFSServiceType.java new file mode 100644 index 0000000000..1e5ac9c281 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/service/IPFSServiceType.java @@ -0,0 +1,14 @@ +package com.alphawallet.app.service; + +import com.alphawallet.app.entity.QueryResponse; + +import java.io.IOException; + +/** + * Created by JB on 4/11/2022. + */ +public interface IPFSServiceType +{ + String getContent(String url); + QueryResponse performIO(String url, String[] headers) throws IOException; +} diff --git a/app/src/main/java/com/alphawallet/app/service/KeyService.java b/app/src/main/java/com/alphawallet/app/service/KeyService.java index 0f9ed0eaba..085af98579 100644 --- a/app/src/main/java/com/alphawallet/app/service/KeyService.java +++ b/app/src/main/java/com/alphawallet/app/service/KeyService.java @@ -23,6 +23,7 @@ import android.security.keystore.KeyProperties; import android.security.keystore.StrongBoxUnavailableException; import android.security.keystore.UserNotAuthenticatedException; +import android.util.Pair; import android.widget.Toast; import androidx.annotation.RequiresApi; @@ -53,7 +54,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -66,6 +66,7 @@ import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.security.spec.AlgorithmParameterSpec; import java.util.Enumeration; import javax.crypto.Cipher; @@ -75,6 +76,7 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; import timber.log.Timber; import wallet.core.jni.CoinType; @@ -87,12 +89,14 @@ public class KeyService implements AuthenticationCallback, PinAuthenticationCallbackInterface { private static final String TAG = "HDWallet"; - private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; - private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; - private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; - private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding"; private static final int AUTHENTICATION_DURATION_SECONDS = 30; public static final String FAILED_SIGNATURE = "00000000000000000000000000000000000000000000000000000000000000000"; + private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; + private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; + + public static final String ANDROID_KEY_STORE = "AndroidKeyStore"; + public static final String LEGACY_CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding"; + public static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding"; //This value determines the time interval between the user swiping away the backup warning notice and it re-appearing public static final long TIME_BETWEEN_BACKUP_WARNING_MILLIS = 1000L * 60L * 60L * 24L * 30L; //30 days //1000 * 60 * 3; //3 minutes for testing @@ -108,7 +112,7 @@ public enum UpgradeKeyResultType REQUESTING_SECURITY, NO_SCREENLOCK, ALREADY_LOCKED, ERROR, SUCCESSFULLY_UPGRADED } - public class UpgradeKeyResult + public static class UpgradeKeyResult { public final UpgradeKeyResultType result; public final String message; @@ -510,6 +514,60 @@ private synchronized String unpackMnemonic() throws KeyServiceException, UserNot } } + public Pair testCipher(String walletAddress, String cipherAlgorithm) + { + KeyExceptionType retVal = KeyExceptionType.UNKNOWN; + String keyData = null; + try + { + String encryptedDataFilePath = KeyService.getFilePath(context, walletAddress); + String keyIv = KeyService.getFilePath(context, walletAddress + "iv"); + boolean ivExists = new File(keyIv).exists(); + boolean aliasExists = new File(encryptedDataFilePath).exists(); + + if (!ivExists) + { + retVal = KeyExceptionType.IV_NOT_FOUND; + throw new Exception("iv file doesn't exist"); + } + if (!aliasExists) + { + retVal = KeyExceptionType.ENCRYPTED_FILE_NOT_FOUND; + throw new Exception("Key file doesn't exist"); + } + + //test legacy key + byte[] iv = KeyService.readBytesFromFile(keyIv); + + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); + keyStore.load(null); + SecretKey secretKey = (SecretKey) keyStore.getKey(walletAddress, null); + + Cipher outCipher = Cipher.getInstance(cipherAlgorithm); + final AlgorithmParameterSpec spec = cipherAlgorithm.equals(CIPHER_ALGORITHM) ? new GCMParameterSpec(128, iv) : new IvParameterSpec(iv); + outCipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + CipherInputStream cipherInputStream = new CipherInputStream(new FileInputStream(encryptedDataFilePath), outCipher); + byte[] keyBytes = KeyService.readBytesFromStream(cipherInputStream); + keyData = new String(keyBytes); + retVal = KeyExceptionType.SUCCESSFUL_DECODE; + } + catch (UserNotAuthenticatedException e) + { + retVal = KeyExceptionType.REQUIRES_AUTH; + } + catch (InvalidKeyException e) + { + //Wrong spec + retVal = KeyExceptionType.INVALID_CIPHER; + } + catch (Exception e) + { + // Other + } + + return new Pair<>(retVal, keyData); + } + private void createHDKey() { HDWallet newWallet = new HDWallet(DEFAULT_KEY_STRENGTH, ""); @@ -1100,14 +1158,12 @@ private synchronized SignatureFromKey signWithKeystore(byte[] transactionBytes) Utility methods */ - static byte[] readBytesFromFile(String path) + public static byte[] readBytesFromFile(String path) { byte[] bytes = null; - FileInputStream fin; - try + File file = new File(path); + try (FileInputStream fin = new FileInputStream(file)) { - File file = new File(path); - fin = new FileInputStream(file); bytes = readBytesFromStream(fin); } catch (IOException e) @@ -1117,6 +1173,22 @@ static byte[] readBytesFromFile(String path) return bytes; } + public static byte[] readBytesFromStream(InputStream in) throws IOException + { + ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); + int bufferSize = 2048; + byte[] buffer = new byte[bufferSize]; + + int len; + while ((len = in.read(buffer)) != -1) + { + byteBuffer.write(buffer, 0, len); + } + + byteBuffer.close(); + return byteBuffer.toByteArray(); + } + /** * Finds matching key in keystore regardless of case * @@ -1148,7 +1220,7 @@ private String findMatchingAddrInKeyStore(String keyAddress) return keyAddress; } - synchronized static String getFilePath(Context context, String fileName) + public synchronized static String getFilePath(Context context, String fileName) { //check for matching file File check = new File(context.getFilesDir(), fileName); @@ -1174,85 +1246,18 @@ synchronized static String getFilePath(Context context, String fileName) private boolean writeBytesToFile(String path, byte[] data) { - FileOutputStream fos = null; - try + File file = new File(path); + try (FileOutputStream fos = new FileOutputStream(file)) { - File file = new File(path); - fos = new FileOutputStream(file); - // Writes bytes from the specified byte array to this file output stream fos.write(data); - return true; - } - catch (FileNotFoundException e) - { - Timber.d("File not found" + e); - } - catch (IOException ioe) - { - Timber.d(ioe, "Exception while writing file "); - } - finally - { - // close the streams using close method - try - { - if (fos != null) - { - fos.close(); - } - } - catch (IOException ioe) - { - Timber.d("Error while closing stream: " + ioe); - } - } - return false; - } - - static byte[] readBytesFromStream(InputStream in) - { - // this dynamically extends to take the bytes you read - ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); - // this is storage overwritten on each iteration with bytes - int bufferSize = 1024; - byte[] buffer = new byte[bufferSize]; - // we need to know how may bytes were read to write them to the byteBuffer - int len; - try - { - while ((len = in.read(buffer)) != -1) - { - byteBuffer.write(buffer, 0, len); - } } catch (IOException e) { - e.printStackTrace(); - } - finally - { - try - { - byteBuffer.close(); - } - catch (IOException e) - { - e.printStackTrace(); - } - if (in != null) - { - try - { - in.close(); - } - catch (IOException e) - { - e.printStackTrace(); - } - } + Timber.d(e, "Exception while writing file "); + return false; } - // and then we can return your byte array. - return byteBuffer.toByteArray(); + + return true; } /** @@ -1388,4 +1393,31 @@ private void vibrate() } } } + + public boolean hasKeystore(String walletAddress) + { + try + { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); + keyStore.load(null); + String matchingAddr = findMatchingAddrInKeyStore(walletAddress); + return keyStore.containsAlias(matchingAddr); + } + catch (KeyStoreException|NoSuchAlgorithmException|CertificateException|IOException e) + { + Timber.e(e); + } + + return false; + } + + static boolean hasStrongbox() + { + return securityStatus == SecurityStatus.HAS_STRONGBOX; + } + + public enum KeyExceptionType + { + UNKNOWN, REQUIRES_AUTH, INVALID_CIPHER, SUCCESSFUL_DECODE, IV_NOT_FOUND, ENCRYPTED_FILE_NOT_FOUND + } } diff --git a/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java b/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java index 1044355545..c18ffa7030 100644 --- a/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java +++ b/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java @@ -1,9 +1,9 @@ package com.alphawallet.app.service; +import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; + import android.text.TextUtils; -import android.util.Log; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; @@ -48,8 +48,6 @@ import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; - public class KeystoreAccountService implements AccountKeystoreService { public static final String KEYSTORE_FOLDER = "keystore/keystore"; @@ -60,7 +58,8 @@ public class KeystoreAccountService implements AccountKeystoreService private final KeyService keyService; private static final ObjectMapper objectMapper = new ObjectMapper(); - public KeystoreAccountService(File keyStoreFile, File baseFile, KeyService keyService) { + public KeystoreAccountService(File keyStoreFile, File baseFile, KeyService keyService) + { keyFolder = keyStoreFile; databaseFolder = baseFile; this.keyService = keyService; @@ -76,35 +75,40 @@ public KeystoreAccountService(File keyStoreFile, File baseFile, KeyService keySe /** * No longer used; keep for testing + * * @param password account password * @return */ @Override - public Single createAccount(String password) { + public Single createAccount(String password) + { return Single.fromCallable(() -> { - ECKeyPair ecKeyPair = Keys.createEcKeyPair(); - WalletFile walletFile = org.web3j.crypto.Wallet.createLight(password, ecKeyPair); - return objectMapper.writeValueAsString(walletFile); - }).compose(upstream -> importKeystore(upstream.blockingGet(), password, password)) - .subscribeOn(Schedulers.io()); + ECKeyPair ecKeyPair = Keys.createEcKeyPair(); + WalletFile walletFile = org.web3j.crypto.Wallet.createLight(password, ecKeyPair); + return objectMapper.writeValueAsString(walletFile); + }).compose(upstream -> importKeystore(upstream.blockingGet(), password, password)) + .subscribeOn(Schedulers.io()); } /** * Import Keystore - * @param store store to include - * @param password store password + * + * @param store store to include + * @param password store password * @param newPassword * @return */ @Override - public Single importKeystore(String store, String password, String newPassword) { + public Single importKeystore(String store, String password, String newPassword) + { return Single.fromCallable(() -> { String address = extractAddressFromStore(store); Wallet wallet; //delete old account files - these have had their password overwritten. If present user chose to refresh key deleteAccountFiles(address); - try { + try + { WalletFile walletFile = objectMapper.readValue(store, WalletFile.class); ECKeyPair kp = org.web3j.crypto.Wallet.decrypt(password, walletFile); Credentials credentials = Credentials.create(kp); @@ -120,7 +124,9 @@ public Single importKeystore(String store, String password, String newPa wallet = new Wallet(credentials.getAddress()); wallet.setWalletType(WalletType.KEYSTORE); - } catch (Exception ex) { + } + catch (Exception ex) + { // We need to make sure that we do not have a broken account deleteAccount(address, newPassword).subscribe(() -> {}, t -> {}).isDisposed(); throw ex; @@ -130,11 +136,15 @@ public Single importKeystore(String store, String password, String newPa }).subscribeOn(Schedulers.io()); } - private String extractAddressFromStore(String store) throws Exception { - try { + private String extractAddressFromStore(String store) throws Exception + { + try + { JSONObject jsonObject = new JSONObject(store); return "0x" + Numeric.cleanHexPrefix(jsonObject.getString("address")); - } catch (JSONException ex) { + } + catch (JSONException ex) + { throw new Exception("Invalid keystore"); } } @@ -147,7 +157,8 @@ private String extractAddressFromStore(String store) throws Exception { * @return */ @Override - public Single importPrivateKey(String privateKey, String newPassword) { + public Single importPrivateKey(String privateKey, String newPassword) + { return Single.fromCallable(() -> { BigInteger key = new BigInteger(privateKey, PRIVATE_KEY_RADIX); ECKeyPair keypair = ECKeyPair.create(key); @@ -157,7 +168,8 @@ public Single importPrivateKey(String privateKey, String newPassword) { } @Override - public Single exportAccount(Wallet wallet, String password, String newPassword) { + public Single exportAccount(Wallet wallet, String password, String newPassword) + { return Single .fromCallable(() -> getCredentials(keyFolder, wallet.address, password)) .map(credentials -> org.web3j.crypto.Wallet.createLight(newPassword, credentials.getEcKeyPair())) @@ -168,12 +180,14 @@ public Single exportAccount(Wallet wallet, String password, String newPa /** * Delete 'geth' keystore file then ensure password encrypted bytes and keys in Android keystore * are deleted - * @param address account address + * + * @param address account address * @param password account password * @return */ @Override - public Completable deleteAccount(String address, String password) { + public Completable deleteAccount(String address, String password) + { return Completable.fromAction(() -> { String cleanedAddr = Numeric.cleanHexPrefix(address).toLowerCase(); deleteAccountFiles(cleanedAddr); @@ -194,7 +208,7 @@ public Completable deleteAccount(String address, String password) { //Now delete all traces of the key in Android keystore, encrypted bytes and iv file in private data area keyService.deleteKey(address); - } ); + }); } private void deleteAccountFiles(String address) @@ -221,7 +235,9 @@ private void deleteRecursive(File fp) if (contents != null) { for (File child : contents) + { deleteRecursive(child); + } } } @@ -229,22 +245,23 @@ private void deleteRecursive(File fp) } @Override - public Single signTransaction(Wallet signer, String toAddress, BigInteger amount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId) { + public Single signTransaction(Wallet signer, String toAddress, BigInteger amount, BigInteger gasPrice, BigInteger gasLimit, long nonce, byte[] data, long chainId) + { return Single.fromCallable(() -> { - RawTransaction rtx = formatRawTransaction(toAddress, amount, gasPrice, gasLimit, nonce, data); - byte[] signData = TransactionEncoder.encode(rtx, chainId); - SignatureFromKey returnSig = keyService.signData(signer, signData); - Sign.SignatureData sigData = sigFromByteArray(returnSig.signature); - if (sigData == null) - { - returnSig.sigType = SignatureReturnType.KEY_CIPHER_ERROR; - returnSig.failMessage = "Incorrect signature length"; //should never see this message - } - else sigData = TransactionEncoder.createEip155SignatureData(sigData, chainId); - returnSig.signature = encode(rtx, sigData); - return returnSig; - }) - .subscribeOn(Schedulers.io()); + RawTransaction rtx = formatRawTransaction(toAddress, amount, gasPrice, gasLimit, nonce, data); + byte[] signData = TransactionEncoder.encode(rtx, chainId); + SignatureFromKey returnSig = keyService.signData(signer, signData); + Sign.SignatureData sigData = sigFromByteArray(returnSig.signature); + if (sigData == null) + { + returnSig.sigType = SignatureReturnType.KEY_CIPHER_ERROR; + returnSig.failMessage = "Incorrect signature length"; //should never see this message + } + else sigData = TransactionEncoder.createEip155SignatureData(sigData, chainId); + returnSig.signature = encode(rtx, sigData); + return returnSig; + }) + .subscribeOn(Schedulers.io()); } @Override @@ -270,7 +287,8 @@ public Single signTransactionEIP1559(Wallet signer, String toA SignatureFromKey returnSig = keyService.signData(signer, signData); sigData = sigFromByteArray(returnSig.signature); - if (sigData == null) { + if (sigData == null) + { returnSig.sigType = SignatureReturnType.KEY_CIPHER_ERROR; returnSig.failMessage = "Incorrect signature length"; //should never see this message } @@ -279,11 +297,13 @@ public Single signTransactionEIP1559(Wallet signer, String toA }).subscribeOn(Schedulers.io()); } - private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) { + private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) + { List values = TransactionEncoder.asRlpValues(rawTransaction, signatureData); RlpList rlpList = new RlpList(values); byte[] encoded = RlpEncoder.encode(rlpList); - if (!rawTransaction.getType().equals(TransactionType.LEGACY)) { + if (!rawTransaction.getType().equals(TransactionType.LEGACY)) + { return ByteBuffer.allocate(encoded.length + 1) .put(rawTransaction.getType().getRlpType()) .put(encoded) @@ -294,6 +314,7 @@ private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData s /** * Get web3j credentials + * * @param keyFolder KeyStore Folder * @param address * @param password @@ -325,8 +346,28 @@ public static Credentials getCredentials(File keyFolder, String address, String return credentials; } + public static Credentials getCredentialsWithThrow(File keyFolder, String address, String password) throws Exception + { + Credentials credentials = null; + + address = Numeric.cleanHexPrefix(address); + File[] contents = keyFolder.listFiles(); + for (File f : contents) + { + if (f.getName().contains(address)) + { + credentials = WalletUtils.loadCredentials(password, f); + break; + } + } + + Timber.tag("RealmDebug").d("gotcredentials + %s", address); + return credentials; + } + @Override - public Single signTransactionFast(Wallet signer, String signerPassword, byte[] message, long chainId) { + public Single signMessageFast(Wallet signer, String signerPassword, byte[] message) + { return Single.fromCallable(() -> { Credentials credentials = getCredentials(keyFolder, signer.address, signerPassword); Sign.SignatureData signatureData = Sign.signMessage( @@ -338,10 +379,9 @@ public Single signTransactionFast(Wallet signer, String signerPassword, } @Override - public Single signMessage(Wallet signer, Signable message, long chainId) + public Single signMessage(Wallet signer, Signable message) { return Single.fromCallable(() -> { - //byte[] messageHash = Hash.sha3(message); SignatureFromKey returnSig = keyService.signData(signer, message.getPrehash()); returnSig.signature = patchSignatureVComponent(returnSig.signature); return returnSig; @@ -349,7 +389,8 @@ public Single signMessage(Wallet signer, Signable message, lon } @Override - public boolean hasAccount(String address) { + public boolean hasAccount(String address) + { address = Numeric.cleanHexPrefix(address); File[] contents = keyFolder.listFiles(); if (contents == null) return false; @@ -365,44 +406,45 @@ public boolean hasAccount(String address) { } @Override - public Single fetchAccounts() { + public Single fetchAccounts() + { return Single.fromCallable(() -> { - File[] contents = keyFolder.listFiles(); - List fileDates = new ArrayList<>(); - Map walletMap = new HashMap<>(); - List wallets = new ArrayList<>(); - if (contents == null || contents.length == 0) return new Wallet[0]; - //Wallet[] result = new Wallet[contents.length]; - for (File f : contents) - { - String fName = f.getName(); - int index = fName.lastIndexOf("-"); - String address = "0x" + fName.substring(index + 1); - if (Utils.isAddressValid(address)) - { - String d = fName.substring(5, index-1).replace("T", " ").substring(0, 23); - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS", Locale.ROOT); - Date date = simpleDateFormat.parse(d); - fileDates.add(date); - walletMap.put(date, address); - } - } + File[] contents = keyFolder.listFiles(); + List fileDates = new ArrayList<>(); + Map walletMap = new HashMap<>(); + List wallets = new ArrayList<>(); + if (contents == null || contents.length == 0) return new Wallet[0]; + //Wallet[] result = new Wallet[contents.length]; + for (File f : contents) + { + String fName = f.getName(); + int index = fName.lastIndexOf("-"); + String address = "0x" + fName.substring(index + 1); + if (Utils.isAddressValid(address)) + { + String d = fName.substring(5, index - 1).replace("T", " ").substring(0, 23); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS", Locale.ROOT); + Date date = simpleDateFormat.parse(d); + fileDates.add(date); + walletMap.put(date, address); + } + } - Collections.sort(fileDates); + Collections.sort(fileDates); - //now build a date sorted array: - for (Date d : fileDates) - { - String address = walletMap.get(d); - Wallet wallet = new Wallet(address); - wallet.type = WalletType.KEYSTORE; - wallet.walletCreationTime = d.getTime(); - wallets.add(wallet); - } + //now build a date sorted array: + for (Date d : fileDates) + { + String address = walletMap.get(d); + Wallet wallet = new Wallet(address); + wallet.type = WalletType.KEYSTORE; + wallet.walletCreationTime = d.getTime(); + wallets.add(wallet); + } - return wallets.toArray(new Wallet[0]); - }) - .subscribeOn(Schedulers.io()); + return wallets.toArray(new Wallet[0]); + }) + .subscribeOn(Schedulers.io()); } /** @@ -420,7 +462,7 @@ private byte[] patchSignatureVComponent(byte[] signature) { if (signature != null && signature.length == 65 && signature[64] < 27) { - signature[64] = (byte)(signature[64] + (byte)0x1b); + signature[64] = (byte) (signature[64] + (byte) 0x1b); } return signature; diff --git a/app/src/main/java/com/alphawallet/app/service/LegacyKeystore.java b/app/src/main/java/com/alphawallet/app/service/LegacyKeystore.java index 44f51a3ae9..ea3c3d3c6d 100644 --- a/app/src/main/java/com/alphawallet/app/service/LegacyKeystore.java +++ b/app/src/main/java/com/alphawallet/app/service/LegacyKeystore.java @@ -1,30 +1,33 @@ package com.alphawallet.app.service; +import static com.alphawallet.app.entity.ServiceErrorException.ServiceErrorCode; +import static com.alphawallet.app.entity.ServiceErrorException.ServiceErrorCode.KEY_IS_GONE; + import android.content.Context; import android.security.keystore.UserNotAuthenticatedException; import com.alphawallet.app.R; import com.alphawallet.app.entity.ServiceErrorException; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.security.*; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; -import static com.alphawallet.app.entity.ServiceErrorException.*; -import static com.alphawallet.app.entity.ServiceErrorException.ServiceErrorCode.KEY_IS_GONE; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; public class LegacyKeystore { - private static final String LEGACY_CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding"; - private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; - public static synchronized byte[] getLegacyPassword( final Context context, String keyName) @@ -34,7 +37,7 @@ public static synchronized byte[] getLegacyPassword( String encryptedDataFilePath = KeyService.getFilePath(context, keyName); try { - keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); + keyStore = KeyStore.getInstance(KeyService.ANDROID_KEY_STORE); keyStore.load(null); SecretKey secretKey = (SecretKey) keyStore.getKey(keyName, null); if (secretKey == null) @@ -64,7 +67,7 @@ public static synchronized byte[] getLegacyPassword( { throw new NullPointerException(context.getString(R.string.cannot_read_encrypt_file)); } - Cipher outCipher = Cipher.getInstance(LEGACY_CIPHER_ALGORITHM); + Cipher outCipher = Cipher.getInstance(KeyService.LEGACY_CIPHER_ALGORITHM); outCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); CipherInputStream cipherInputStream = new CipherInputStream(new FileInputStream(encryptedDataFilePath), outCipher); return KeyService.readBytesFromStream(cipherInputStream); diff --git a/app/src/main/java/com/alphawallet/app/service/OpenSeaService.java b/app/src/main/java/com/alphawallet/app/service/OpenSeaService.java index 1798d07bad..08a7f3682f 100644 --- a/app/src/main/java/com/alphawallet/app/service/OpenSeaService.java +++ b/app/src/main/java/com/alphawallet/app/service/OpenSeaService.java @@ -12,6 +12,7 @@ import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenFactory; import com.alphawallet.app.entity.tokens.TokenInfo; +import com.alphawallet.app.repository.KeyProviderFactory; import com.alphawallet.app.util.JsonUtils; import com.alphawallet.ethereum.EthereumNetworkBase; import com.google.gson.Gson; @@ -38,20 +39,13 @@ public class OpenSeaService { - private static OkHttpClient httpClient; + private final OkHttpClient httpClient; private static final int PAGE_SIZE = 50; private final Map imageUrls = new HashMap<>(); private static final TokenFactory tf = new TokenFactory(); private final LongSparseArray networkCheckTimes = new LongSparseArray<>(); private final LongSparseArray pageOffsets = new LongSparseArray<>(); - static - { - System.loadLibrary("keys"); - } - - public static native String getOpenSeaKey(); - public OpenSeaService() { pageOffsets.clear(); @@ -67,12 +61,13 @@ private Request buildRequest(long networkId, String api) { Request.Builder requestB = new Request.Builder() .url(api) - .header("User-Agent", "Chrome/74.0.3729.169") - .method("GET", null) - .addHeader("Content-Type", "application/json"); + .method("GET", null); - String apiKey = getOpenSeaKey(); - if (networkId != EthereumNetworkBase.RINKEBY_ID && !TextUtils.isEmpty(apiKey) && !apiKey.equals("...")) + String apiKey = KeyProviderFactory.get().getOpenSeaKey(); + if (!TextUtils.isEmpty(apiKey) + && !apiKey.equals("...") + && com.alphawallet.app.repository.EthereumNetworkBase.hasRealValue(networkId) + ) { requestB.addHeader("X-API-KEY", apiKey); } @@ -94,7 +89,6 @@ private String executeRequest(long networkId, String api) } else { - Timber.d(response.toString()); return JsonUtils.EMPTY_RESULT; } } @@ -341,8 +335,15 @@ public boolean canCheckChain(long networkId) public Single getAsset(Token token, BigInteger tokenId) { - return Single.fromCallable(() -> - fetchAsset(token.tokenInfo.chainId, token.tokenInfo.address, tokenId.toString())); + if (!com.alphawallet.app.repository.EthereumNetworkBase.hasOpenseaAPI(token.tokenInfo.chainId)) + { + return Single.fromCallable(() -> ""); + } + else + { + return Single.fromCallable(() -> + fetchAsset(token.tokenInfo.chainId, token.tokenInfo.address, tokenId.toString())); + } } public Single getCollection(Token token, String slug) @@ -355,19 +356,41 @@ public String fetchAssets(long networkId, String address, int offset) { String api = ""; String ownerOption = "owner"; + + //TODO: Put these into a mapping if (networkId == EthereumNetworkBase.MAINNET_ID) { api = C.OPENSEA_ASSETS_API_MAINNET; } - else if (networkId == EthereumNetworkBase.RINKEBY_ID) + else if (networkId == EthereumNetworkBase.GOERLI_ID) { - api = C.OPENSEA_ASSETS_API_RINKEBY; + api = C.OPENSEA_ASSETS_API_TESTNET; } - else if (networkId == EthereumNetworkBase.MATIC_ID) + else if (networkId == EthereumNetworkBase.POLYGON_ID) { api = C.OPENSEA_ASSETS_API_MATIC; ownerOption = "owner_address"; } + else if (networkId == EthereumNetworkBase.ARBITRUM_MAIN_ID) + { + api = C.OPENSEA_ASSETS_API_ARBITRUM; + ownerOption = "owner_address"; + } + else if (networkId == EthereumNetworkBase.AVALANCHE_ID) + { + api = C.OPENSEA_ASSETS_API_AVALANCHE; + ownerOption = "owner_address"; + } + else if (networkId == EthereumNetworkBase.KLAYTN_ID) + { + api = C.OPENSEA_ASSETS_API_KLAYTN; + ownerOption = "owner_address"; + } + else if (networkId == EthereumNetworkBase.OPTIMISTIC_MAIN_ID) + { + api = C.OPENSEA_ASSETS_API_OPTIMISM; + ownerOption = "owner_address"; + } Uri.Builder builder = new Uri.Builder(); builder.encodedPath(api) @@ -385,14 +408,30 @@ public String fetchAsset(long networkId, String contractAddress, String tokenId) { api = C.OPENSEA_SINGLE_ASSET_API_MAINNET + contractAddress + "/" + tokenId; } - else if (networkId == EthereumNetworkBase.RINKEBY_ID) + else if (networkId == EthereumNetworkBase.GOERLI_ID) { - api = C.OPENSEA_SINGLE_ASSET_API_RINKEBY + contractAddress + "/" + tokenId; + api = C.OPENSEA_SINGLE_ASSET_API_TESTNET + contractAddress + "/" + tokenId; } - else if (networkId == EthereumNetworkBase.MATIC_ID) + else if (networkId == EthereumNetworkBase.POLYGON_ID) { api = C.OPENSEA_SINGLE_ASSET_API_MATIC + contractAddress + "/" + tokenId; } + else if (networkId == EthereumNetworkBase.ARBITRUM_MAIN_ID) + { + api = C.OPENSEA_SINGLE_ASSET_API_ARBITRUM + contractAddress + "/" + tokenId; + } + else if (networkId == EthereumNetworkBase.AVALANCHE_ID) + { + api = C.OPENSEA_SINGLE_ASSET_API_AVALANCHE + contractAddress + "/" + tokenId; + } + else if (networkId == EthereumNetworkBase.KLAYTN_ID) + { + api = C.OPENSEA_SINGLE_ASSET_API_KLAYTN + contractAddress + "/" + tokenId; + } + else if (networkId == EthereumNetworkBase.OPTIMISTIC_MAIN_ID) + { + api = C.OPENSEA_SINGLE_ASSET_API_OPTIMISM + contractAddress + "/" + tokenId; + } return executeRequest(networkId, api); } diff --git a/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java b/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java index f88028858d..1eb02a6d95 100644 --- a/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java +++ b/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java @@ -5,6 +5,8 @@ import android.os.Binder; import android.os.IBinder; +import androidx.annotation.Nullable; + import com.alphawallet.app.R; import com.alphawallet.app.entity.CurrencyItem; import com.alphawallet.app.entity.Wallet; @@ -26,8 +28,6 @@ import javax.inject.Inject; -import androidx.annotation.Nullable; - import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; diff --git a/app/src/main/java/com/alphawallet/app/service/SwapService.java b/app/src/main/java/com/alphawallet/app/service/SwapService.java index c1a623a713..fab5804e2e 100644 --- a/app/src/main/java/com/alphawallet/app/service/SwapService.java +++ b/app/src/main/java/com/alphawallet/app/service/SwapService.java @@ -3,24 +3,29 @@ import android.net.Uri; import com.alphawallet.app.C; -import com.alphawallet.app.entity.lifi.Connection; +import com.alphawallet.app.entity.lifi.RouteOptions; +import com.alphawallet.app.entity.lifi.Token; +import com.alphawallet.app.repository.SwapRepository; import com.alphawallet.app.util.BalanceUtils; import com.alphawallet.app.util.JsonUtils; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; import io.reactivex.Single; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.ResponseBody; import timber.log.Timber; public class SwapService { - private static final String FETCH_CHAINS = "https://li.quest/v1/chains"; - private static final String FETCH_TOKENS = "https://li.quest/v1/connections"; - private static final String SWAP_TOKEN = "https://li.quest/v1/quote"; private static OkHttpClient httpClient; public SwapService() @@ -38,14 +43,23 @@ private Request buildRequest(String api) Request.Builder requestB = new Request.Builder() .url(api) .header("User-Agent", "Chrome/74.0.3729.169") - .method("GET", null) - .addHeader("Content-Type", "application/json"); + .addHeader("Content-Type", "application/json") + .get(); + return requestB.build(); + } + + private Request buildPostRequest(String api, RequestBody requestBody) + { + Request.Builder requestB = new Request.Builder() + .url(api) + .header("User-Agent", "Chrome/74.0.3729.169") + .addHeader("Content-Type", "application/json") + .post(requestBody); return requestB.build(); } private String executeRequest(String api) { - Timber.d(api); try (okhttp3.Response response = httpClient.newCall(buildRequest(api)).execute()) { if (response.isSuccessful()) @@ -70,49 +84,185 @@ private String executeRequest(String api) return JsonUtils.EMPTY_RESULT; } + private String executePostRequest(String api, RequestBody requestBody) + { + try (okhttp3.Response response = httpClient.newCall(buildPostRequest(api, requestBody)).execute()) + { + if (response.isSuccessful()) + { + ResponseBody responseBody = response.body(); + if (responseBody != null) + { + return responseBody.string(); + } + } + else + { + return Objects.requireNonNull(response.body()).string(); + } + } + catch (Exception e) + { + Timber.e(e); + return e.getMessage(); + } + + return JsonUtils.EMPTY_RESULT; + } + public Single getChains() { return Single.fromCallable(this::fetchChains); } + public Single getTools() + { + return Single.fromCallable(this::fetchTools); + } + public Single getConnections(long from, long to) { return Single.fromCallable(() -> fetchPairs(from, to)); } - public Single getQuote(Connection.LToken source, Connection.LToken dest, String address, String amount, String slippage) + public Single getQuote(Token source, + Token dest, + String address, + String amount, + String slippage, + String allowExchanges) + { + return Single.fromCallable(() -> fetchQuote(source, dest, address, amount, slippage, allowExchanges)); + } + + public Single getRoutes(Token source, + Token dest, + String address, + String amount, + String slippage, + Set exchanges) { - return Single.fromCallable(() -> fetchQuote(source, dest, address, amount, slippage)); + return Single.fromCallable(() -> fetchRoutes(source, dest, address, amount, slippage, exchanges)); + } + + public Single getRoutes(String fromChainId, + String toChainId, + String fromTokenAddress, + String toTokenAddress, + String fromAddress, + String fromAmount, + String slippage, + Set exchanges) + { + return Single.fromCallable(() -> fetchRoutes(fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAddress, fromAmount, slippage, exchanges)); } public String fetchChains() { Uri.Builder builder = new Uri.Builder(); - builder.encodedPath(FETCH_CHAINS); + builder.encodedPath(SwapRepository.FETCH_CHAINS); + return executeRequest(builder.build().toString()); + } + + public String fetchTools() + { + Uri.Builder builder = new Uri.Builder(); + builder.encodedPath(SwapRepository.FETCH_TOOLS); return executeRequest(builder.build().toString()); } public String fetchPairs(long fromChain, long toChain) { Uri.Builder builder = new Uri.Builder(); - builder.encodedPath(FETCH_TOKENS) + builder.encodedPath(SwapRepository.FETCH_TOKENS) .appendQueryParameter("fromChain", String.valueOf(fromChain)) .appendQueryParameter("toChain", String.valueOf(toChain)); return executeRequest(builder.build().toString()); } - public String fetchQuote(Connection.LToken source, Connection.LToken dest, String address, String amount, String slippage) + public String fetchQuote(Token source, + Token dest, + String address, + String amount, + String slippage, + String allowExchanges) { Uri.Builder builder = new Uri.Builder(); - builder.encodedPath(SWAP_TOKEN) + builder.encodedPath(SwapRepository.FETCH_QUOTE) .appendQueryParameter("fromChain", String.valueOf(source.chainId)) .appendQueryParameter("toChain", String.valueOf(dest.chainId)) .appendQueryParameter("fromToken", source.address) .appendQueryParameter("toToken", dest.address) .appendQueryParameter("fromAddress", address) .appendQueryParameter("fromAmount", BalanceUtils.getRawFormat(amount, source.decimals)) -// .appendQueryParameter("order", "RECOMMENDED") + .appendQueryParameter("allowExchanges", allowExchanges) .appendQueryParameter("slippage", slippage); return executeRequest(builder.build().toString()); } + + public String fetchRoutes(Token source, + Token dest, + String address, + String amount, + String slippage, + Set exchanges) + { + RouteOptions options = new RouteOptions(); + options.slippage = slippage; + options.exchanges.allow.addAll(exchanges); + + RequestBody body = null; + try + { + JSONObject json = new JSONObject(); + json.put("fromChainId", String.valueOf(source.chainId)); + json.put("toChainId", String.valueOf(dest.chainId)); + json.put("fromTokenAddress", source.address); + json.put("toTokenAddress", dest.address); + json.put("fromAddress", address); + json.put("fromAmount", BalanceUtils.getRawFormat(amount, source.decimals)); + json.put("options", options.getJson()); + body = RequestBody.create(json.toString(), MediaType.parse("application/json")); + } + catch (JSONException e) + { + Timber.e(e); + } + + return executePostRequest(SwapRepository.FETCH_ROUTES, body); + } + + public String fetchRoutes(String fromChainId, + String toChainId, + String fromTokenAddress, + String toTokenAddress, + String fromAddress, + String fromAmount, + String slippage, + Set exchanges) + { + RouteOptions options = new RouteOptions(); + options.slippage = slippage; + options.exchanges.allow.addAll(exchanges); + + RequestBody body = null; + try + { + JSONObject json = new JSONObject(); + json.put("fromChainId", fromChainId); + json.put("toChainId", toChainId); + json.put("fromTokenAddress", fromTokenAddress); + json.put("toTokenAddress", toTokenAddress); + json.put("fromAddress", fromAddress); + json.put("fromAmount", fromAmount); + json.put("options", options.getJson()); + body = RequestBody.create(json.toString(), MediaType.parse("application/json")); + } + catch (JSONException e) + { + Timber.e(e); + } + + return executePostRequest(SwapRepository.FETCH_ROUTES, body); + } } diff --git a/app/src/main/java/com/alphawallet/app/service/TickerService.java b/app/src/main/java/com/alphawallet/app/service/TickerService.java index fa1c1cbed8..0351db96e6 100644 --- a/app/src/main/java/com/alphawallet/app/service/TickerService.java +++ b/app/src/main/java/com/alphawallet/app/service/TickerService.java @@ -9,16 +9,16 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.CLASSIC_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.CRONOS_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.FANTOM_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.GNOSIS_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.HECO_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.IOTEX_MAINNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.KLAYTN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MILKOMEDA_C1_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISTIC_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.POA_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.RINKEBY_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_TEST_ID; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; import android.text.TextUtils; @@ -79,17 +79,18 @@ public class TickerService { private static final int UPDATE_TICKER_CYCLE = 5; //5 Minutes private static final String MEDIANIZER = "0x729D19f657BD0614b4985Cf1D82531c67569197B"; - private static final String MARKET_ORACLE_CONTRACT = "0xf155a7eb4a2993c8cf08a76bca137ee9ac0a01d8"; + private static final String MARKET_ORACLE_CONTRACT = "0xdAcAf435f241B1a062B021abEED9CA2F76F22F8D"; private static final String CONTRACT_ADDR = "[CONTRACT_ADDR]"; private static final String CHAIN_IDS = "[CHAIN_ID]"; private static final String CURRENCY_TOKEN = "[CURRENCY]"; private static final String COINGECKO_CHAIN_CALL = "https://api.coingecko.com/api/v3/simple/price?ids=" + CHAIN_IDS + "&vs_currencies=" + CURRENCY_TOKEN + "&include_24hr_change=true"; - private static final String COINGECKO_API = "https://api.coingecko.com/api/v3/simple/token_price/" + CHAIN_IDS + "?contract_addresses=" + CONTRACT_ADDR + "&vs_currencies=" + CURRENCY_TOKEN + "&include_24hr_change=true"; + private static final String COINGECKO_API = String.format("https://api.coingecko.com/api/v3/simple/token_price/%s?contract_addresses=%s&vs_currencies=%s&include_24hr_change=true", + CHAIN_IDS, CONTRACT_ADDR, CURRENCY_TOKEN); private static final String DEXGURU_API = "https://api.dex.guru/v1/tokens/" + CONTRACT_ADDR + "-" + CHAIN_IDS; private static final String CURRENCY_CONV = "currency"; private static final boolean ALLOW_UNVERIFIED_TICKERS = false; //allows verified:false tickers from DEX.GURU. Not recommended public static final long TICKER_TIMEOUT = DateUtils.WEEK_IN_MILLIS; //remove ticker if not seen in one week - public static final long TICKER_STALE_TIMEOUT = 15 * DateUtils.MINUTE_IN_MILLIS; //try to use market API if AlphaWallet market oracle not updating + public static final long TICKER_STALE_TIMEOUT = 30 * DateUtils.MINUTE_IN_MILLIS; //Use market API if AlphaWallet market oracle not updating private final OkHttpClient httpClient; private final PreferenceRepositoryType sharedPrefs; @@ -100,6 +101,7 @@ public class TickerService private static String currentCurrencySymbol; private static final Map canUpdate = new ConcurrentHashMap<>(); private static final Map dexGuruQuery = new ConcurrentHashMap<>(); + private static long lastTickerUpdate; @Nullable private Disposable tickerUpdateTimer; @@ -118,11 +120,12 @@ public TickerService(OkHttpClient httpClient, PreferenceRepositoryType sharedPre resetTickerUpdate(); initCurrency(); + lastTickerUpdate = 0; } public void updateTickers() { - if (mainTickerUpdate != null && !mainTickerUpdate.isDisposed()) + if (mainTickerUpdate != null && !mainTickerUpdate.isDisposed() && System.currentTimeMillis() > (lastTickerUpdate + DateUtils.MINUTE_IN_MILLIS)) { return; //do not update if update is currently in progress } @@ -130,8 +133,8 @@ public void updateTickers() sharedPrefs.commit(); tickerUpdateTimer = Observable.interval(0, UPDATE_TICKER_CYCLE, TimeUnit.MINUTES) - .doOnNext(l -> tickerUpdate()) - .subscribe(); + .doOnNext(l -> tickerUpdate()) + .subscribe(); } private void tickerUpdate() @@ -150,6 +153,7 @@ private void tickersUpdated(int tickerCount) { Timber.d("Tickers Updated: %s", tickerCount); mainTickerUpdate = null; + lastTickerUpdate = System.currentTimeMillis(); } public Single updateCurrencyConversion() @@ -164,13 +168,22 @@ private Double storeCurrentRate(Double rate) if (rate == 0.0) { TokenTicker tt = localSource.getCurrentTicker(TokensRealmSource.databaseKey(0, CURRENCY_CONV)); - if (tt != null) { return Double.parseDouble(tt.price); } - else { return 0.0; } + if (tt != null) + { + return Double.parseDouble(tt.price); + } + else + { + return 0.0; + } } else { TokenTicker currencyTicker = new TokenTicker(Double.toString(rate), "0", currentCurrencySymbolTxt, null, System.currentTimeMillis()); - localSource.updateERC20Tickers(0, new HashMap() {{ put(CURRENCY_CONV, currencyTicker); }}); + localSource.updateERC20Tickers(0, new HashMap() + {{ + put(CURRENCY_CONV, currencyTicker); + }}); return rate; } } @@ -210,7 +223,7 @@ private Single fetchCoinGeckoChainPrices() } } } - catch (IOException e) + catch (Exception e) { Timber.e(e); } @@ -225,7 +238,7 @@ private Single updateTickersFromOracle(double conversionRate) currentConversionRate = conversionRate; return Single.fromCallable(() -> { int tickerSize = 0; - final Web3j web3j = TokenRepository.getWeb3jService(RINKEBY_ID); + final Web3j web3j = TokenRepository.getWeb3jService(POLYGON_TEST_ID); //fetch current tickers Function function = getTickers(); String responseValue = callSmartContractFunction(web3j, function, MARKET_ORACLE_CONTRACT); @@ -271,7 +284,7 @@ public Single syncERC20Tickers(long chainId, List erc20T for (TokenCardMeta tcm : erc20Tokens) { if (!dexGuruQuery.containsKey(tcm.tokenId) // don't include any token in the dexguru queue - && (!currentTickerMap.containsKey(tcm.getAddress()) + && (!currentTickerMap.containsKey(tcm.getAddress()) || currentTickerMap.get(tcm.getAddress()) < staleTime)) //include tokens who's tickers have gone stale { lookupMap.put(tcm.getAddress().toLowerCase(), tcm); @@ -312,7 +325,10 @@ private Map fetchERC20TokenTickers(long chainId, Collection if (apiChainName == null) return erc20Tickers; final Map lookupMap = new HashMap<>(); - for (TokenCardMeta tcm : erc20Tokens) { lookupMap.put(tcm.getAddress().toLowerCase(), tcm); } + for (TokenCardMeta tcm : erc20Tokens) + { + lookupMap.put(tcm.getAddress().toLowerCase(), tcm); + } //build ticker header StringBuilder sb = new StringBuilder(); @@ -509,7 +525,8 @@ private TokenTicker decodeCoinGeckoTicker(JSONObject eth) fiatPrice = eth.getDouble("usd") * currentConversionRate; fiatChangeStr = eth.getString("usd_24h_change"); } - if (!TextUtils.isEmpty(fiatChangeStr) && Character.isDigit(fiatChangeStr.charAt(0))) changeValue = BigDecimal.valueOf(eth.getDouble(currentCurrencySymbolTxt.toLowerCase() + "_24h_change")); + if (!TextUtils.isEmpty(fiatChangeStr) && Character.isDigit(fiatChangeStr.charAt(0))) + changeValue = BigDecimal.valueOf(eth.getDouble(currentCurrencySymbolTxt.toLowerCase() + "_24h_change")); tTicker = new TokenTicker(String.valueOf(fiatPrice), changeValue.setScale(3, RoundingMode.DOWN).toString(), currentCurrencySymbolTxt, "", System.currentTimeMillis()); @@ -526,14 +543,14 @@ private TokenTicker decodeCoinGeckoTicker(JSONObject eth) public Single convertPair(String currency1, String currency2) { return Single.fromCallable(() -> { - if (currency1 == null || currency2 == null || currency1.equals(currency2)) return (Double)1.0; + if (currency1 == null || currency2 == null || currency1.equals(currency2)) return (Double) 1.0; String conversionURL = "http://currencies.apps.grandtrunk.net/getlatest/" + currency1 + "/" + currency2; double rate = 0.0; Request request = new Request.Builder() .url(conversionURL) - .addHeader("Connection","close") + .addHeader("Connection", "close") .get() .build(); @@ -548,7 +565,7 @@ public Single convertPair(String currency1, String currency2) } catch (Exception e) { - e.printStackTrace(); + Timber.e(e); rate = 0.0; } @@ -556,8 +573,8 @@ public Single convertPair(String currency1, String currency2) }); } - private String callSmartContractFunction(Web3j web3j, - Function function, String contractAddress) throws Exception { + private String callSmartContractFunction(Web3j web3j, Function function, String contractAddress) + { String encodedFunction = FunctionEncoder.encode(function); try @@ -575,20 +592,24 @@ private String callSmartContractFunction(Web3j web3j, } catch (Exception e) { - e.printStackTrace(); + Timber.e(e); return null; } } - private static Function getTickers() { + private static Function getTickers() + { return new Function( "getTickers", Arrays.asList(), - Collections.singletonList(new TypeReference>() {})); + Collections.singletonList(new TypeReference>() + { + })); } /** * Potentially used by forks to add a custom ticker + * * @param chainId * @param ticker */ @@ -603,6 +624,7 @@ public void addCustomTicker(long chainId, TokenTicker ticker) /** * Potentially used by forks + * * @param chainId * @param address * @param ticker @@ -613,12 +635,14 @@ public void addCustomTicker(long chainId, String address, TokenTicker ticker) if (ticker != null && address != null) { Single.fromCallable(() -> { - localSource.updateERC20Tickers(chainId, new HashMap() - {{ put(address, ticker); }}); - return true; - }).subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe().isDisposed(); + localSource.updateERC20Tickers(chainId, new HashMap() + {{ + put(address, ticker); + }}); + return true; + }).subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe().isDisposed(); } } @@ -667,6 +691,7 @@ private void initCurrency() /** * Returns the current ISO currency string eg EUR, AUD etc. + * * @return 3 character currency ISO text */ public static String getCurrencySymbolTxt() @@ -692,11 +717,12 @@ private void resetTickerUpdate() } // Update this list from here: https://api.coingecko.com/api/v3/asset_platforms - public static final Map coinGeckoChainIdToAPIName = new HashMap(){{ + public static final Map coinGeckoChainIdToAPIName = new HashMap() + {{ put(MAINNET_ID, "ethereum"); - put(XDAI_ID, "xdai"); + put(GNOSIS_ID, "xdai"); put(BINANCE_MAIN_ID, "binance-smart-chain"); - put(MATIC_ID, "polygon-pos"); + put(POLYGON_ID, "polygon-pos"); put(CLASSIC_ID, "ethereum-classic"); put(FANTOM_ID, "fantom"); put(AVALANCHE_ID, "avalanche"); @@ -714,10 +740,11 @@ private void resetTickerUpdate() put(CRONOS_MAIN_ID, "cronos"); }}; - private static final Map dexGuruChainIdToAPISymbol = new HashMap(){{ + private static final Map dexGuruChainIdToAPISymbol = new HashMap() + {{ put(MAINNET_ID, "eth"); put(BINANCE_MAIN_ID, "bsc"); - put(MATIC_ID, "polygon"); + put(POLYGON_ID, "polygon"); put(AVALANCHE_ID, "avalanche"); }}; @@ -727,16 +754,17 @@ public void deleteTickers() } // Update from https://api.coingecko.com/api/v3/coins/list - public static final Map chainPairs = new HashMap(){{ + public static final Map chainPairs = new HashMap() + {{ put(MAINNET_ID, "ethereum"); put(CLASSIC_ID, "ethereum-classic"); put(POA_ID, "poa-network"); - put(XDAI_ID, "xdai"); + put(GNOSIS_ID, "xdai"); put(BINANCE_MAIN_ID, "binancecoin"); put(HECO_ID, "huobi-token"); put(AVALANCHE_ID, "avalanche-2"); put(FANTOM_ID, "fantom"); - put(MATIC_ID, "matic-network"); + put(POLYGON_ID, "matic-network"); put(ARBITRUM_MAIN_ID, "ethereum"); put(OPTIMISTIC_MAIN_ID, "ethereum"); put(KLAYTN_ID, "klay-token"); @@ -759,7 +787,10 @@ private String getCoinGeckoChainCall() boolean firstPair = true; for (long chainId : chainPairs.keySet()) { - if (ethTickers.containsKey(chainId)) { continue; } + if (ethTickers.containsKey(chainId)) + { + continue; + } if (!firstPair) tokenList.append(","); firstPair = false; tokenList.append(chainPairs.get(chainId)); @@ -772,7 +803,10 @@ private boolean receivedAllChainPairs() { for (long chainId : chainPairs.keySet()) { - if (!ethTickers.containsKey(chainId)) { return false; } + if (!ethTickers.containsKey(chainId)) + { + return false; + } } return true; diff --git a/app/src/main/java/com/alphawallet/app/service/TokensService.java b/app/src/main/java/com/alphawallet/app/service/TokensService.java index 29a8a283cb..959e4b11b6 100644 --- a/app/src/main/java/com/alphawallet/app/service/TokensService.java +++ b/app/src/main/java/com/alphawallet/app/service/TokensService.java @@ -10,7 +10,7 @@ import androidx.annotation.Nullable; import com.alphawallet.app.BuildConfig; -import com.alphawallet.app.C; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.ContractType; @@ -21,6 +21,7 @@ import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokendata.TokenGroup; import com.alphawallet.app.entity.tokendata.TokenTicker; +import com.alphawallet.app.entity.tokendata.TokenUpdateType; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.entity.tokens.TokenFactory; @@ -134,14 +135,14 @@ private void checkUnknownTokens() if (t != null && t.address.length() > 0 && (cachedToken == null || TextUtils.isEmpty(cachedToken.tokenInfo.name))) { - queryUnknownTokensDisposable = tokenRepository.update(t.address, t.chainId).toObservable() //fetch tokenInfo - .filter(tokenInfo -> (!TextUtils.isEmpty(tokenInfo.name) || !TextUtils.isEmpty(tokenInfo.symbol)) && tokenInfo.chainId != 0) - .map(tokenInfo -> { tokenInfo.isEnabled = false; return tokenInfo; }) //set default visibility to false - .flatMap(tokenInfo -> tokenRepository.determineCommonType(tokenInfo).toObservable() - .map(contractType -> tokenFactory.createToken(tokenInfo, contractType, ethereumNetworkRepository.getNetworkByChain(t.chainId).getShortName()))) + ContractType type = tokenRepository.determineCommonType(new TokenInfo(t.address, "", "", 18, false, t.chainId)).blockingGet(); + + queryUnknownTokensDisposable = tokenRepository.update(t.address, t.chainId, type) //fetch tokenInfo + .map(tokenInfo -> tokenFactory.createToken(tokenInfo, type, ethereumNetworkRepository.getNetworkByChain(t.chainId).getShortName())) + .flatMap(token -> tokenRepository.updateTokenBalance(currentAddress, token)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) - .subscribe(this::finishAddToken, err -> onCheckError(err, t), this::finishTokenCheck); + .subscribe(this::finishAddToken, err -> onCheckError(err, t)); } else if (t == null) { @@ -157,19 +158,11 @@ private void onCheckError(Throwable throwable, ContractAddress t) Timber.e(throwable); } - private void finishTokenCheck() + private void finishAddToken(BigDecimal balance) { queryUnknownTokensDisposable = null; } - private void finishAddToken(Token token) - { - if (token != null && token.getInterfaceSpec() != ContractType.OTHER) - { - tokenStoreList.add(token); - } - } - public Token getToken(long chainId, String addr) { if (TextUtils.isEmpty(currentAddress) || TextUtils.isEmpty(addr)) return null; @@ -179,10 +172,10 @@ public Token getToken(long chainId, String addr) public void storeToken(Token token) { if (TextUtils.isEmpty(currentAddress) || token == null || token.getInterfaceSpec() == ContractType.OTHER) return; - tokenStoreDisposable = tokenRepository.checkInterface(new Token[] { token }, new Wallet(token.getWallet())) + tokenStoreDisposable = tokenRepository.checkInterface(token, new Wallet(token.getWallet())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(tkn -> Collections.addAll(tokenStoreList, tkn), this::onERC20Error); + .subscribe(tokenStoreList::add, this::onERC20Error); } public TokenTicker getTokenTicker(Token token) @@ -215,6 +208,7 @@ public void setCurrentAddress(String newWalletAddr) stopUpdateCycle(); addLockedTokens(); if (openseaService != null) openseaService.resetOffsetRead(networkFilter); + tokenRepository.updateLocalAddress(newWalletAddr); } } @@ -239,7 +233,6 @@ public void startUpdateCycle() eventTimer = Single.fromCallable(() -> { startupPass(); - addUnresolvedContracts(ethereumNetworkRepository.getAllKnownContracts(getNetworkFilters())); checkIssueTokens(); pendingTokenMap.clear(); return true; @@ -524,8 +517,8 @@ public void addTokenImageUrl(long networkId, String address, String imageUrl) tokenRepository.addImageUrl(networkId, address, imageUrl); } - public Single update(String address, long chainId) { - return tokenRepository.update(address, chainId) + public Single update(String address, long chainId, ContractType type) { + return tokenRepository.update(address, chainId, type) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } @@ -547,21 +540,6 @@ private void checkIssueTokens() .isDisposed(); } - private void addUnresolvedContracts(List contractCandidates) - { - if (openseaService == null) return; //no need for this if syncing - if (contractCandidates != null && contractCandidates.size() > 0) - { - for (ContractLocator cl : contractCandidates) - { - if (getToken(cl.chainId, cl.address) == null) - { - addUnknownTokenToCheck(new ContractAddress(cl.chainId, cl.address)); - } - } - } - } - private void checkTokensBalance() { final Token t = getNextInBalanceUpdateQueue(); @@ -583,10 +561,43 @@ public Single getChainBalance(String walletAddress, long chainId) return tokenRepository.fetchChainBalance(walletAddress, chainId); } + // Note that this routine works across different wallets, so there's no usage of currentAddress + public Single syncChainBalances(String walletAddress, TokenUpdateType updateType) + { + //update all chain balances + return Single.fromCallable(() -> { + List baseTokens = new ArrayList<>(); + for (long chainId : networkFilter) + { + Token baseToken = tokenRepository.fetchToken(chainId, walletAddress, walletAddress); + if (baseToken == null) + { + baseToken = ethereumNetworkRepository.getBlankOverrideToken(ethereumNetworkRepository.getNetworkByChain(chainId)); + } + baseToken.setTokenWallet(walletAddress); + BigDecimal balance = baseToken.balance; + if (updateType == TokenUpdateType.ACTIVE_SYNC) balance = tokenRepository.updateTokenBalance(walletAddress, baseToken).blockingGet(); + if (balance.compareTo(BigDecimal.ZERO) > 0) + { + baseToken.balance = balance; + baseTokens.add(baseToken); + } + } + + return baseTokens.toArray(new Token[0]); + }); + } + private void onBalanceChange(BigDecimal newBalance, Token t) { boolean balanceChange = !newBalance.equals(t.balance); + if (newBalance.equals(BigDecimal.valueOf(-2))) + { + //token deleted + return; + } + if (balanceChange && BuildConfig.DEBUG) { Timber.tag(TAG).d("Change Registered: * %s", t.getFullName()); @@ -649,40 +660,51 @@ private void checkOpenSea(long chainId) NetworkInfo info = ethereumNetworkRepository.getNetworkByChain(chainId); if (info.chainId == transferCheckChain) return; //currently checking this chainId in TransactionsNetworkClient - - final Wallet wallet = new Wallet(currentAddress); Timber.tag(TAG).d("Fetch from opensea : " + currentAddress + " : " + info.getShortName()); openSeaCheckId = info.chainId; - openSeaQueryDisposable = openseaService.getTokens(currentAddress, info.chainId, info.getShortName(), this) - .flatMap(tokens -> tokenRepository.checkInterface(tokens, wallet)) //check the token interface - .map(tokens -> tokenRepository.initNFTAssets(wallet, tokens)) - .flatMap(tokens -> tokenRepository.storeTokens(wallet, tokens)) //store fetched tokens + openSeaQueryDisposable = callOpenSeaAPI(info) .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(t -> checkedNetwork(info), - this::chuckError); + .observeOn(Schedulers.io()) + .subscribe(r -> { + openSeaQueryDisposable = null; + openSeaCheckId = 0; + }, this::openSeaCallError); } - public boolean openSeaUpdateInProgress(long chainId) + private void openSeaCallError(Throwable error) { - return openSeaQueryDisposable != null && !openSeaQueryDisposable.isDisposed() && openSeaCheckId == chainId; + Timber.w(error); + openSeaQueryDisposable = null; + openSeaCheckId = 0; } - private void checkedNetwork(NetworkInfo info) + private Single callOpenSeaAPI(NetworkInfo info) { - openSeaQueryDisposable = null; - openSeaCheckId = 0; - Timber.tag(TAG).d("Checked " + info.name + " Opensea"); + final Wallet wallet = new Wallet(currentAddress); + + return Single.fromCallable(() -> { + openseaService.getTokens(currentAddress, info.chainId, info.getShortName(), this).toObservable() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .flatMap(Observable::fromArray) + .blockingForEach(t -> tokenRepository.checkInterface(t, wallet) + .map(token -> tokenRepository.initNFTAssets(wallet, token)) + .flatMap(token -> tokenRepository.storeTokens(wallet, new Token[]{token})) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .blockingGet() + ); + + return true; + }); } - private void chuckError(@NotNull Throwable e) + public boolean openSeaUpdateInProgress(long chainId) { - openSeaCheckId = 0; - openSeaQueryDisposable = null; - Timber.e(e); + return openSeaQueryDisposable != null && !openSeaQueryDisposable.isDisposed() && openSeaCheckId == chainId; } private void checkERC20(long chainId) @@ -826,7 +848,7 @@ public Token getNextInBalanceUpdateQueue() //simply multiply the weighting by the last diff. float updateFactor = weighting * (float) lastCheckDiff * (check.isEnabled ? 1 : 0.25f); - long cutoffCheck = 30*DateUtils.SECOND_IN_MILLIS / (check.isEnabled ? 1 : 10); //normal minimum update frequency for token 30 seconds, 5 minutes for hidden token + long cutoffCheck = check.calculateUpdateFrequency(); //normal minimum update frequency for token 30 seconds, 5 minutes for hidden token if (!check.isEthereum() && lastUpdateDiff > DateUtils.DAY_IN_MILLIS) { @@ -992,55 +1014,11 @@ private Completable enableToken(String walletAddr, Token token) }); } - public Completable lockTokenVisibility(Token token) - { - return enableToken(currentAddress, token); - } - - /** - * Timings for when there can be a check for new transactions - * @param t - * @return - */ - private long getTokenTimeInterval(Token t) - { - long nextTimeCheck; - - if (t.isEthereum()) - { - nextTimeCheck = 30*DateUtils.SECOND_IN_MILLIS; //allow base chains to be checked about every 30 seconds - } - else - { - nextTimeCheck = t.getTransactionCheckInterval(); - } - - return nextTimeCheck; - } - public Realm getTickerRealmInstance() { return tokenRepository.getTickerRealmInstance(); } - public boolean shouldDisplayPopularToken(TokenCardMeta tcm) - { - //Display popular token if - // - explicitly enabled - // - user has not altered the visibility and token has positive balance (user may not be aware of visibility controls). - if (ethereumNetworkRepository.getIsPopularToken(tcm.getChain(), tcm.getAddress())) - { - Token token = getToken(tcm.getChain(), tcm.getAddress()); - return (token == null) - || tokenRepository.isEnabled(token) - || (!tokenRepository.hasVisibilityBeenChanged(token) && token.hasPositiveBalance()); - } - else - { - return true; - } - } - public void walletInFocus() { appHasFocus = true; @@ -1068,12 +1046,12 @@ public void track(String gasSpeed) if (analyticsService != null) { AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(gasSpeed); - - analyticsService.track(C.AN_USE_GAS, analyticsProperties); + analyticsProperties.put(Analytics.PROPS_GAS_SPEED, gasSpeed); + analyticsService.track(Analytics.Action.USE_GAS_WIDGET.getValue(), analyticsProperties); } } + @NotNull public Token getTokenOrBase(long chainId, String address) { Token token = getToken(chainId, address); @@ -1237,4 +1215,16 @@ public boolean hasLockedGas(long chainId) { return ethereumNetworkRepository.hasLockedGas(chainId); } + + public Single deleteTokens(List metasToDelete) + { + return Single.fromCallable(() -> { + tokenRepository.deleteRealmTokens(new Wallet(currentAddress), metasToDelete); + for (TokenCardMeta tcm : metasToDelete) + { + pendingTokenMap.remove(databaseKey(tcm.getChain(), tcm.getAddress())); + } + return true; + }); + } } diff --git a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java index 7e207e2648..075c927184 100644 --- a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java +++ b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java @@ -7,9 +7,8 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_MAINNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_TESTNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_TEST_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_TEST_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_TEST_ID; import android.text.TextUtils; @@ -27,11 +26,14 @@ import com.alphawallet.app.entity.tokens.ERC721Token; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenInfo; +import com.alphawallet.app.repository.KeyProvider; +import com.alphawallet.app.repository.KeyProviderFactory; import com.alphawallet.app.repository.TransactionsRealmCache; import com.alphawallet.app.repository.entity.RealmAuxData; import com.alphawallet.app.repository.entity.RealmToken; import com.alphawallet.app.repository.entity.RealmTransaction; import com.alphawallet.app.repository.entity.RealmTransfer; +import com.alphawallet.app.util.Utils; import com.alphawallet.token.entity.ContractAddress; import com.google.gson.Gson; @@ -39,7 +41,6 @@ import org.json.JSONException; import org.json.JSONObject; -import java.io.InterruptedIOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; @@ -68,40 +69,30 @@ public class TransactionsNetworkClient implements TransactionsNetworkClientType private final String BLOCK_ENTRY = "-erc20blockCheck-"; private final String ERC20_QUERY = "tokentx"; private final String ERC721_QUERY = "tokennfttx"; - private final int AUX_DATABASE_ID = 26; //increment this to do a one off refresh the AUX database, in case of changed design etc + private final int AUX_DATABASE_ID = 27; //increment this to do a one off refresh the AUX database, in case of changed design etc private final String DB_RESET = BLOCK_ENTRY + AUX_DATABASE_ID; private final String ETHERSCAN_API_KEY; private final String BSC_EXPLORER_API_KEY; private final String POLYGONSCAN_API_KEY; private final String AURORASCAN_API_KEY; + private final KeyProvider keyProvider = KeyProviderFactory.get(); private final OkHttpClient httpClient; private final Gson gson; private final RealmManager realmManager; - static { - System.loadLibrary("keys"); - } - - public static native String getEtherscanKey(); - public static native String getBSCExplorerKey(); - public static native String getCovalentKey(); - public static native String getPolygonScanKey(); - public static native String getAuroraScanKey(); - public TransactionsNetworkClient( OkHttpClient httpClient, Gson gson, - RealmManager realmManager - ) { + RealmManager realmManager) { this.httpClient = httpClient; this.gson = gson; this.realmManager = realmManager; - BSC_EXPLORER_API_KEY = getBSCExplorerKey().length() > 0 ? "&apikey=" + getBSCExplorerKey() : ""; - ETHERSCAN_API_KEY = "&apikey=" + getEtherscanKey(); - POLYGONSCAN_API_KEY = getPolygonScanKey().length() > 3 ? "&apikey=" + getPolygonScanKey() : ""; - AURORASCAN_API_KEY = getAuroraScanKey().length() > 3 ? "&apikey=" + getAuroraScanKey() : ""; + BSC_EXPLORER_API_KEY = keyProvider.getBSCExplorerKey().length() > 0 ? "&apikey=" + keyProvider.getBSCExplorerKey() : ""; + ETHERSCAN_API_KEY = "&apikey=" + keyProvider.getEtherscanKey(); + POLYGONSCAN_API_KEY = keyProvider.getPolygonScanKey().length() > 3 ? "&apikey=" + keyProvider.getPolygonScanKey() : ""; + AURORASCAN_API_KEY = keyProvider.getAuroraScanKey().length() > 3 ? "&apikey=" + keyProvider.getAuroraScanKey() : ""; } @Override @@ -176,7 +167,7 @@ public Single storeNewTransactions(TokensService svs, NetworkInfo { lastTransaction = syncDownwards(updates, instance, svs, networkInfo, tokenAddress, 9999999999L); } - else // try to sydenc upwards from the last read + else // try to sync upwards from the last read { lastTransaction = syncUpwards(updates, instance, svs, networkInfo, tokenAddress, lastBlockNumber); } @@ -432,6 +423,11 @@ private EtherscanTransaction[] readTransactions(NetworkInfo networkInfo, TokensS fullUrl = sb.toString(); + if (networkInfo.isCustom && !Utils.isValidUrl(networkInfo.etherscanAPI)) + { + return new EtherscanTransaction[0]; + } + Request request = new Request.Builder() .url(fullUrl) .get() @@ -451,12 +447,6 @@ private EtherscanTransaction[] readTransactions(NetworkInfo networkInfo, TokensS return getEtherscanTransactions(result); } } - catch (InterruptedIOException e) - { - //If user switches account or network during a fetch - //this exception is going to be thrown because we're terminating the API call - //Don't display error - } catch (Exception e) { Timber.e(e); @@ -582,6 +572,7 @@ private void writeTokens(String walletAddress, NetworkInfo networkInfo, Ethersca token.getInterfaceSpec() != ContractType.ERC721_UNDETERMINED ))) { token = createNewERC721Token(eventMap.get(contract).get(0), networkInfo, walletAddress, false); + token.setTokenWallet(walletAddress); newToken = true; Timber.tag(TAG).d("Discover NFT: " + ev0.tokenName + " (" + ev0.tokenSymbol + ")"); } @@ -605,10 +596,6 @@ else if (token == null) { writeAssets(eventMap, token, walletAddress, contract, svs, newToken); } - else if (newToken) // new Fungible token - { - svs.storeToken(token); - } else { //instruct tokensService to update balance @@ -632,12 +619,13 @@ private int calcTokenDecimals(EtherscanEvent ev0) return tokenDecimal; } - private void writeAssets(Map> eventMap, Token token, String walletAddress, - String contractAddress, TokensService svs, boolean newToken) + private void writeAssets (Map> eventMap, Token token, String walletAddress, + String contractAddress, TokensService svs, boolean newToken) { - List additions = new ArrayList<>(); - List removals = new ArrayList<>(); + HashSet additions = new HashSet<>(); + HashSet removals = new HashSet<>(); + //run through addition/removal in chronological order for (EtherscanEvent ev : eventMap.get(contractAddress)) { BigInteger tokenId = getTokenId(ev.tokenID); @@ -646,12 +634,12 @@ private void writeAssets(Map> eventMap, Token token if (ev.to.equalsIgnoreCase(walletAddress)) { - if (!additions.contains(tokenId)) { additions.add(tokenId); } + additions.add(tokenId); removals.remove(tokenId); } else { - if (!removals.contains(tokenId)) { removals.add(tokenId); } + removals.add(tokenId); additions.remove(tokenId); } } @@ -663,7 +651,7 @@ private void writeAssets(Map> eventMap, Token token if (additions.size() > 0 || removals.size() > 0) { - svs.updateAssets(token, additions, removals); + svs.updateAssets(token, new ArrayList<>(additions), new ArrayList<>(removals)); } } @@ -679,6 +667,11 @@ private String readNextTxBatch(String walletAddress, NetworkInfo networkInfo, lo "&page=1&offset=" + TRANSFER_RESULT_MAX + "&sort=asc" + getNetworkAPIToken(networkInfo); + if (networkInfo.isCustom && !Utils.isValidUrl(networkInfo.etherscanAPI)) + { + return "0"; + } + Request request = new Request.Builder() .url(fullUrl) .header("User-Agent", "Chrome/74.0.3729.169") @@ -694,15 +687,9 @@ private String readNextTxBatch(String walletAddress, NetworkInfo networkInfo, lo result = "0"; } } - catch (InterruptedIOException e) - { - //If user switches account or network during a fetch - //this exception is going to be thrown because we're terminating the API call - //Don't display error - } catch (Exception e) { - if (networkInfo.chainId != ARTIS_TAU1_ID && BuildConfig.DEBUG) e.printStackTrace(); + if (networkInfo.chainId != ARTIS_TAU1_ID && BuildConfig.DEBUG) Timber.e(e); } return result; @@ -714,18 +701,17 @@ private String getNetworkAPIToken(NetworkInfo networkInfo) { return ETHERSCAN_API_KEY; } - else if (networkInfo.chainId == BINANCE_TEST_ID || networkInfo.chainId == BINANCE_MAIN_ID) + else if (networkInfo.chainId == BINANCE_MAIN_ID) { return BSC_EXPLORER_API_KEY; } - else if (networkInfo.chainId == MATIC_ID || networkInfo.chainId == MATIC_TEST_ID) + else if (networkInfo.chainId == POLYGON_ID || networkInfo.chainId == POLYGON_TEST_ID) { - return POLYGONSCAN_API_KEY; } else if (networkInfo.chainId == AURORA_MAINNET_ID || networkInfo.chainId == AURORA_TESTNET_ID) { - return AURORASCAN_API_KEY; + return AURORASCAN_API_KEY; } else { @@ -736,7 +722,8 @@ else if (networkInfo.chainId == AURORA_MAINNET_ID || networkInfo.chainId == AURO private EtherscanTransaction[] readCovalentTransactions(TokensService svs, String accountAddress, NetworkInfo networkInfo, boolean ascending, int page, int pageSize) throws JSONException { String covalent = "" + networkInfo.chainId + "/address/" + accountAddress.toLowerCase() + "/transactions_v2/?"; - String args = "block-signed-at-asc=" + (ascending ? "true" : "false") + "&page-number=" + (page - 1) + "&page-size=" + pageSize + "&key=" + getCovalentKey(); //read logs to get all the transfers + String args = "block-signed-at-asc=" + (ascending ? "true" : "false") + "&page-number=" + (page - 1) + "&page-size=" + + pageSize + "&key=" + keyProvider.getCovalentKey(); //read logs to get all the transfers String fullUrl = networkInfo.etherscanAPI.replace(COVALENT, covalent); String result = null; @@ -756,12 +743,6 @@ private EtherscanTransaction[] readCovalentTransactions(TokensService svs, Strin return new EtherscanTransaction[0]; } } - catch (InterruptedIOException e) - { - //If user switches account or network during a fetch - //this exception is going to be thrown because we're terminating the API call - //Don't display error - } catch (Exception e) { Timber.e(e); @@ -986,8 +967,8 @@ private void resetBlockRead(Realm r, long chainId, String walletAddress) } } - private void writeEvents(Realm instance, EtherscanEvent[] events, String walletAddress, - @NonNull NetworkInfo networkInfo, final boolean isNFT) throws Exception + private void writeEvents (Realm instance, EtherscanEvent[] events, String walletAddress, + @NonNull NetworkInfo networkInfo, final boolean isNFT) throws Exception { String TO_TOKEN = "[TO_ADDRESS]"; String FROM_TOKEN = "[FROM_ADDRESS]"; @@ -1000,12 +981,14 @@ private void writeEvents(Realm instance, EtherscanEvent[] events, String walletA //write event list for (EtherscanEvent ev : events) { - boolean scanAsNFT = isNFT || ((ev.tokenDecimal == null || ev.tokenDecimal.length() == 0 || ev.tokenDecimal.equals("0")) && (ev.tokenID != null && ev.tokenID.length() > 0)); + boolean scanAsNFT = isNFT || ((ev.tokenDecimal == null || ev.tokenDecimal.length() == 0 || ev.tokenDecimal.equals("0")) && + (ev.tokenID != null && ev.tokenID.length() > 0)); Transaction tx = scanAsNFT ? ev.createNFTTransaction(networkInfo) : ev.createTransaction(networkInfo); //find tx name String activityName = tx.getEventName(walletAddress); - String valueList = VALUES.replace(TO_TOKEN, ev.to).replace(FROM_TOKEN, ev.from).replace(AMOUNT_TOKEN, scanAsNFT ? ev.tokenID : ev.value); //Etherscan sometimes interprets NFT transfers as FT's + //Etherscan sometimes interprets NFT transfers as FT's + String valueList = VALUES.replace(TO_TOKEN, ev.to).replace(FROM_TOKEN, ev.from).replace(AMOUNT_TOKEN, scanAsNFT ? ev.tokenID : ev.value); storeTransferData(r, tx.hash, valueList, activityName, ev.contractAddress); //ensure we have fetched the transaction for each hash writeTransaction(r, tx, ev.contractAddress, networkInfo.etherscanAPI.contains(COVALENT) ? null : txFetches); @@ -1133,4 +1116,4 @@ private ERC721Token createNewERC721Token(EtherscanEvent ev, NetworkInfo networkI newToken.setTokenWallet(walletAddress); return newToken; } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/service/TransactionsService.java b/app/src/main/java/com/alphawallet/app/service/TransactionsService.java index c7326eb8f0..636b405f14 100644 --- a/app/src/main/java/com/alphawallet/app/service/TransactionsService.java +++ b/app/src/main/java/com/alphawallet/app/service/TransactionsService.java @@ -55,15 +55,19 @@ public class TransactionsService private final TransactionLocalSource transactionsCache; private int currentChainIndex; private boolean nftCheck; + private boolean firstCycle; private final LongSparseArray chainTransferCheckTimes = new LongSparseArray<>(); //TODO: Use this to coordinate token checks on chains private final LongSparseArray chainTransactionCheckTimes = new LongSparseArray<>(); - private static final LongSparseArray currentBlocks = new LongSparseArray<>(); + private static final LongSparseArray currentBlocks = new LongSparseArray<>(); private static final ConcurrentLinkedQueue requiredTransactions = new ConcurrentLinkedQueue<>(); private final static int TRANSACTION_DROPPED = -1; private final static int TRANSACTION_SEEN = -2; + private final static long START_CHECK_DELAY = 3; + private final static long CHECK_CYCLE = 15; + @Nullable private Disposable fetchTransactionDisposable; @Nullable @@ -95,7 +99,8 @@ private void fetchTransactions() if (TextUtils.isEmpty(tokensService.getCurrentAddress())) return; currentChainIndex = 0; - nftCheck = true; //check nft first to filter out NFT tokens + nftCheck = false; + firstCycle = true; transactionsClient.checkRequiresAuxReset(tokensService.getCurrentAddress()); @@ -105,23 +110,28 @@ private void fetchTransactions() //reset transaction timers if (transactionCheckCycle == null || transactionCheckCycle.isDisposed()) { - transactionCheckCycle = Observable.interval(0, 15, TimeUnit.SECONDS) + transactionCheckCycle = Observable.interval(START_CHECK_DELAY, CHECK_CYCLE, TimeUnit.SECONDS) .doOnNext(l -> checkTransactionQueue()).subscribe(); } - if (tokenTransferCheckCycle == null || tokenTransferCheckCycle.isDisposed()) - { - tokenTransferCheckCycle = Observable.interval(2, 17, TimeUnit.SECONDS) //attempt to not interfere with transaction check - .doOnNext(l -> checkTransfers()).subscribe(); - } + readTransferCycle(); if (pendingTransactionCheckCycle == null || pendingTransactionCheckCycle.isDisposed()) { - pendingTransactionCheckCycle = Observable.interval(15, 15, TimeUnit.SECONDS) + pendingTransactionCheckCycle = Observable.interval(CHECK_CYCLE, CHECK_CYCLE, TimeUnit.SECONDS) .doOnNext(l -> checkPendingTransactions()).subscribe(); } } + private void readTransferCycle() + { + if (tokenTransferCheckCycle != null && !tokenTransferCheckCycle.isDisposed()) tokenTransferCheckCycle.dispose(); + + tokenTransferCheckCycle = Observable.interval(firstCycle ? START_CHECK_DELAY / 3 : (START_CHECK_DELAY * 15) + 1, + firstCycle ? CHECK_CYCLE / 3 : CHECK_CYCLE, TimeUnit.SECONDS) + .doOnNext(l -> checkTransfers()).subscribe(); + } + public void resumeFocus() { if (!Utils.isAddressValid(tokensService.getCurrentAddress())) return; @@ -161,10 +171,6 @@ private void checkTransfers() if (currentChainIndex >= filters.size()) currentChainIndex = 0; readTokenMoves(filters.get(currentChainIndex), nftCheck); //check NFTs for same chain on next iteration or advance to next chain Pair pendingChainData = getNextChainIndex(currentChainIndex, nftCheck, filters); - if (pendingChainData.first != currentChainIndex) - { - updateCurrentBlock(filters.get(currentChainIndex)); - } currentChainIndex = pendingChainData.first; nftCheck = pendingChainData.second; } @@ -180,6 +186,11 @@ private Pair getNextChainIndex(int currentIndex, boolean nftCh { nftCheck = !nftCheck; currentIndex = 0; + if (!nftCheck && firstCycle) + { + firstCycle = false; + readTransferCycle(); + } } NetworkInfo info = ethereumNetworkRepository.getNetworkByChain(filters.get(currentIndex)); if (!nftCheck || info.usesSeparateNFTTransferQuery()) break; @@ -289,13 +300,6 @@ else if (checkTime < oldestCheck) } } - private void updateCurrentBlock(final long chainId) - { - fetchCurrentBlock(chainId).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(blockValue -> currentBlocks.put(chainId, blockValue), onError -> currentBlocks.put(chainId, BigInteger.ZERO)).isDisposed(); - } - private static Single fetchCurrentBlock(final long chainId) { return Single.fromCallable(() -> { @@ -305,7 +309,7 @@ private static Single fetchCurrentBlock(final long chainId) String blockValStr = ethBlock.getBlock().getNumberRaw(); if (!TextUtils.isEmpty(blockValStr) && blockValStr.length() > 2) return Numeric.toBigInt(blockValStr); - else return currentBlocks.get(chainId, BigInteger.ZERO); + else return currentBlocks.get(chainId, new CurrentBlockTime(BigInteger.ZERO)).blockNumber; }); } @@ -422,6 +426,9 @@ private void stopAllChainUpdate() tokensService.checkingChain(0); chainTransferCheckTimes.clear(); chainTransactionCheckTimes.clear(); + + currentChainIndex = 0; + nftCheck = false; } public static void addTransactionHashFetch(String txHash, long chainId, String wallet) @@ -537,14 +544,14 @@ public void markPending(Transaction tx) public static BigInteger getCurrentBlock(long chainId) { - BigInteger currentBlock = currentBlocks.get(chainId, BigInteger.ZERO); - if (currentBlock.equals(BigInteger.ZERO)) + CurrentBlockTime currentBlock = currentBlocks.get(chainId, new CurrentBlockTime(BigInteger.ZERO)); + if (currentBlock.blockReadRequiresUpdate()) { - currentBlock = fetchCurrentBlock(chainId).blockingGet(); + currentBlock = new CurrentBlockTime(fetchCurrentBlock(chainId).blockingGet()); currentBlocks.put(chainId, currentBlock); } - return currentBlock; + return currentBlock.blockNumber; } private void checkPendingTransactions() @@ -642,4 +649,22 @@ private String getNextUncachedTx() return txHashData; } + + private static class CurrentBlockTime + { + public final long readTime; + public final BigInteger blockNumber; + + public CurrentBlockTime(BigInteger blockNo) + { + readTime = System.currentTimeMillis(); + blockNumber = blockNo; + } + + public boolean blockReadRequiresUpdate() + { + // update every 10 seconds if required + return blockNumber.equals(BigInteger.ZERO) || System.currentTimeMillis() > (readTime + 10 * DateUtils.SECOND_IN_MILLIS); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/service/WalletConnectService.java b/app/src/main/java/com/alphawallet/app/service/WalletConnectService.java index 73c6bff9c7..910dc5f936 100644 --- a/app/src/main/java/com/alphawallet/app/service/WalletConnectService.java +++ b/app/src/main/java/com/alphawallet/app/service/WalletConnectService.java @@ -56,6 +56,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { Timber.tag(TAG).d("SERVICE STARTING"); + if (intent == null) + { + return Service.START_STICKY; + } + try { int actionVal = Integer.parseInt(intent.getAction()); diff --git a/app/src/main/java/com/alphawallet/app/service/WalletConnectV2Service.java b/app/src/main/java/com/alphawallet/app/service/WalletConnectV2Service.java new file mode 100644 index 0000000000..2843870357 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/service/WalletConnectV2Service.java @@ -0,0 +1,68 @@ +package com.alphawallet.app.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; + +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; + +import com.alphawallet.app.R; +import com.alphawallet.app.ui.WalletConnectNotificationActivity; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class WalletConnectV2Service extends Service +{ + @Override + public IBinder onBind(Intent intent) + { + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public void onCreate() + { + super.onCreate(); + String CHANNEL_ID = "my_channel_01"; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, + "WalletConnect V2", + NotificationManager.IMPORTANCE_DEFAULT); + + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(channel); + + Intent intent = new Intent(getApplicationContext(), WalletConnectNotificationActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE); + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_logo) + .setContentTitle(getString(R.string.notify_wallet_connect_title)) + .setContentText(getString(R.string.notify_wallet_connect_content)) + .setContentIntent(pendingIntent) + .build(); + + startForeground(1, notification); + notificationManager.notify(1, notification); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + stopForeground(true); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java b/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java index caf5682bae..2c11c8253a 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java @@ -17,11 +17,11 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.ActivityMeta; import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.TransactionMeta; import com.alphawallet.app.entity.Wallet; -import com.alphawallet.app.entity.WalletPage; import com.alphawallet.app.interact.ActivityDataInteract; import com.alphawallet.app.repository.entity.RealmTransaction; import com.alphawallet.app.repository.entity.RealmTransfer; @@ -221,6 +221,7 @@ private void refreshTransactionList() viewModel.prepare(); } + @Override public void resetTokens() { if (adapter != null) @@ -229,8 +230,13 @@ public void resetTokens() adapter.clear(); viewModel.prepare(); } + else + { + requireActivity().recreate(); + } } + @Override public void addedToken(List tokenContracts) { if (adapter != null) adapter.updateItems(tokenContracts); @@ -252,10 +258,11 @@ public void onResume() super.onResume(); if (viewModel == null) { - ((HomeActivity) getActivity()).resetFragment(WalletPage.ACTIVITY); + requireActivity().recreate(); } else { + viewModel.track(Analytics.Navigation.ACTIVITY); viewModel.prepare(); } @@ -298,12 +305,14 @@ public void leaveFocus() if (realm != null && !realm.isClosed()) realm.close(); } + @Override public void resetTransactions() { //called when we just refreshed the database refreshTransactionList(); } + @Override public void scrollToTop() { if (listView != null) listView.smoothScrollToPosition(0); diff --git a/app/src/main/java/com/alphawallet/app/ui/AddCustomRPCNetworkActivity.java b/app/src/main/java/com/alphawallet/app/ui/AddCustomRPCNetworkActivity.java index 0302716a2a..d67622508d 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AddCustomRPCNetworkActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AddCustomRPCNetworkActivity.java @@ -1,5 +1,7 @@ package com.alphawallet.app.ui; +import static java.util.Collections.singletonList; + import android.os.Bundle; import android.os.Handler; import android.text.InputType; @@ -10,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.viewmodel.CustomNetworkViewModel; @@ -22,16 +25,13 @@ import dagger.hilt.android.AndroidEntryPoint; -import static java.util.Collections.singletonList; - @AndroidEntryPoint public class AddCustomRPCNetworkActivity extends BaseActivity implements StandardFunctionInterface { public static final String CHAIN_ID = "chain_id"; - + private final Handler handler = new Handler(); private CustomNetworkViewModel viewModel; - private InputView nameInputView; private InputView rpcUrlInputView; private InputView chainIdInputView; @@ -39,9 +39,8 @@ public class AddCustomRPCNetworkActivity extends BaseActivity implements Standar private InputView blockExplorerUrlInputView; private InputView blockExplorerApiUrl; private MaterialCheckBox testNetCheckBox; - - private final Handler handler = new Handler(); private long chainId; + private boolean isEditMode; @Override protected void onCreate(Bundle savedInstanceState) @@ -83,7 +82,7 @@ protected void onCreate(Bundle savedInstanceState) initViewModel(); chainId = getIntent().getLongExtra(CHAIN_ID, -1); - boolean isEditMode = chainId >= 0; + isEditMode = chainId >= 0; if (isEditMode) { @@ -109,6 +108,13 @@ protected void onCreate(Bundle savedInstanceState) } } + @Override + protected void onResume() + { + super.onResume(); + viewModel.track(Analytics.Navigation.ADD_CUSTOM_NETWORK); + } + private void addFunctionBar(List functionResources) { FunctionButtonBar functionBar = findViewById(R.id.layoutButtons); @@ -221,7 +227,9 @@ public void handleClick(String action, int actionId) if (validateInputs()) { - viewModel.saveNetwork(nameInputView.getText().toString(), + viewModel.saveNetwork( + isEditMode, + nameInputView.getText().toString(), rpcUrlInputView.getText().toString(), Long.parseLong(chainIdInputView.getText().toString()), symbolInputView.getText().toString(), diff --git a/app/src/main/java/com/alphawallet/app/ui/AddEditDappActivity.java b/app/src/main/java/com/alphawallet/app/ui/AddEditDappActivity.java index 0c9d463716..fdfe5ee6ec 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AddEditDappActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AddEditDappActivity.java @@ -3,32 +3,37 @@ import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; -import androidx.annotation.Nullable; - import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; +import com.alphawallet.app.entity.DApp; +import com.alphawallet.app.util.DappBrowserUtils; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.viewmodel.AddEditDappViewModel; import com.alphawallet.app.widget.InputView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import com.alphawallet.app.util.DappBrowserUtils; -import com.alphawallet.app.util.Utils; import java.util.List; -import com.alphawallet.app.R; -import com.alphawallet.app.entity.DApp; -import timber.log.Timber; import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; @AndroidEntryPoint -public class AddEditDappActivity extends BaseActivity { +public class AddEditDappActivity extends BaseActivity +{ public static final String KEY_MODE = "mode"; public static final String KEY_DAPP = "dapp"; public static final int MODE_ADD = 0; public static final int MODE_EDIT = 1; - + private AddEditDappViewModel viewModel; private TextView title; private InputView name; private InputView url; @@ -39,30 +44,31 @@ public class AddEditDappActivity extends BaseActivity { private DApp dapp; @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) + { super.onCreate(savedInstanceState); setContentView(R.layout.activity_add_edit_dapp); toolbar(); setTitle(""); - - title = findViewById(R.id.title); - name = findViewById(R.id.dapp_title); - url = findViewById(R.id.dapp_url); - button = findViewById(R.id.btn_confirm); - icon = findViewById(R.id.icon); + initViews(); + initViewModel(); Intent intent = getIntent(); - if (intent != null) { + if (intent != null) + { mode = intent.getExtras().getInt(KEY_MODE); dapp = (DApp) intent.getExtras().get(KEY_DAPP); - } else { + } + else + { finish(); } String visibleUrl = Utils.getDomainName(dapp.getUrl()); String favicon; - if (!TextUtils.isEmpty(visibleUrl)) { + if (!TextUtils.isEmpty(visibleUrl)) + { favicon = DappBrowserUtils.getIconUrl(visibleUrl); Glide.with(this) .load(favicon) @@ -70,8 +76,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { .into(icon); } - switch (mode) { - case MODE_ADD: { + switch (mode) + { + case MODE_ADD: + { setTitle(getString(R.string.add_to_my_dapps)); button.setText(R.string.action_add); name.setText(dapp.getName()); @@ -81,10 +89,12 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { button.setOnClickListener(v -> { dapp.setName(name.getText().toString()); dapp.setUrl(url.getText().toString()); - add(dapp); }); + add(dapp); + }); break; } - case MODE_EDIT: { + case MODE_EDIT: + { setTitle(getString(R.string.edit_dapp)); button.setText(R.string.action_save); url.setText(dapp.getUrl()); @@ -96,13 +106,36 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { }); break; } - default: { + default: + { break; } } } - private void save(DApp dapp) { + @Override + protected void onResume() + { + viewModel.track(mode == MODE_ADD ? Analytics.Navigation.ADD_DAPP : Analytics.Navigation.EDIT_DAPP); + super.onResume(); + } + + private void initViews() + { + title = findViewById(R.id.title); + name = findViewById(R.id.dapp_title); + url = findViewById(R.id.dapp_url); + button = findViewById(R.id.btn_confirm); + icon = findViewById(R.id.icon); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this).get(AddEditDappViewModel.class); + } + + private void save(DApp dapp) + { try { List myDapps = DappBrowserUtils.getMyDapps(this); @@ -116,6 +149,10 @@ private void save(DApp dapp) { } } DappBrowserUtils.saveToPrefs(this, myDapps); + + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_URL, dapp.getUrl()); + viewModel.track(Analytics.Action.DAPP_EDITED, props); } catch (Exception e) { @@ -127,10 +164,16 @@ private void save(DApp dapp) { } } - private void add(DApp dapp) { + private void add(DApp dapp) + { List myDapps = DappBrowserUtils.getMyDapps(this); myDapps.add(dapp); DappBrowserUtils.saveToPrefs(this, myDapps); + + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_URL, dapp.getUrl()); + viewModel.track(Analytics.Action.DAPP_ADDED, props); + finish(); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java b/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java index 0a2d589716..c2bf49dcc8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java @@ -1,6 +1,7 @@ package com.alphawallet.app.ui; import static com.alphawallet.app.C.ADDED_TOKEN; +import static com.alphawallet.app.C.RESET_WALLET; import static com.alphawallet.app.ui.widget.holder.TokenHolder.CHECK_MARK; import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; @@ -9,7 +10,6 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.util.LongSparseArray; import android.view.Menu; import android.view.MenuItem; @@ -25,7 +25,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.ContractLocator; @@ -39,32 +38,33 @@ import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EthereumNetworkRepository; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.ui.widget.adapter.TokensAdapter; import com.alphawallet.app.ui.widget.entity.AddressReadyCallback; import com.alphawallet.app.util.QRParser; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.AddTokenViewModel; +import com.alphawallet.app.widget.AWBottomSheetDialog; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.FunctionButtonBar; import com.alphawallet.app.widget.InputAddress; +import com.alphawallet.app.widget.TestNetDialog; import com.alphawallet.token.tools.ParseMagicLink; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.inject.Inject; -import timber.log.Timber; - import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; @AndroidEntryPoint -public class AddTokenActivity extends BaseActivity implements AddressReadyCallback, StandardFunctionInterface, TokensAdapterCallback +public class AddTokenActivity extends BaseActivity implements AddressReadyCallback, StandardFunctionInterface, TokensAdapterCallback, TestNetDialog.TestNetDialogCallback { private AddTokenViewModel viewModel; @@ -85,6 +85,7 @@ public class AddTokenActivity extends BaseActivity implements AddressReadyCallba private RecyclerView recyclerView; private AWalletAlertDialog aDialog; + private AWBottomSheetDialog dialog; private final LongSparseArray tokenList = new LongSparseArray<>(); @Override @@ -125,6 +126,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { adapter = new TokensAdapter(this, viewModel.getAssetDefinitionService(), viewModel.getTokensService(), null); adapter.setHasStableIds(true); + adapter.showTestNetTips(); adapter.setFilterType(TokenFilter.NO_FILTER); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(this)); @@ -273,17 +275,22 @@ private void finishAndLaunchSend() } } - private void onSaved(Token result) + private void onSaved() { showProgress(false); - if (result != null) + List selected = adapter.getSelected(); + + if (selected.size() > 0) { - ContractLocator cr = new ContractLocator(result.getAddress(), result.tokenInfo.chainId); + TokenCardMeta result = selected.get(0); + ContractLocator cr = new ContractLocator(result.getAddress(), result.getChain()); Intent iResult = new Intent(); iResult.putParcelableArrayListExtra(ADDED_TOKEN, new ArrayList<>(Collections.singletonList(cr))); + iResult.putExtra(RESET_WALLET, true); setResult(RESULT_OK, iResult); - finish(); } + + finish(); } private void onError(ErrorEnvelope errorEnvelope) { @@ -321,28 +328,114 @@ private void onCheck(String address) lastCheck = address; showProgress(true); progressLayout.setVisibility(View.VISIBLE); + adapter.clear(); viewModel.testNetworks(address); } } - private void onSave() { + private void onSave() + { + boolean mainNetActive = viewModel.ethereumNetworkRepository().isMainNetSelected(); + List activeChains = viewModel.ethereumNetworkRepository().getFilterNetworkList(); List selected = adapter.getSelected(); + HashSet chainsNotEnabled = new HashSet<>(); + boolean onlyTestNet = selected.size() > 0; + + // 1. if any mainnet networks were selected, and we're using testnet, switch to mainnet + // 2. detect if we need to enable chains + for (TokenCardMeta token : selected) + { + NetworkInfo info = viewModel.ethereumNetworkRepository().getNetworkByChain(token.getChain()); + if (info.hasRealValue()) onlyTestNet = false; + if (!activeChains.contains(info.chainId)) + { + chainsNotEnabled.add(info.chainId); + } + } + + if (mainNetActive && onlyTestNet) // only testnet tokens selected and we're not showing testnet + { + //Will need to make these chains active on the callback + TestNetDialog testnetDialog = new TestNetDialog(this, activeChains.get(0), this); + testnetDialog.show(); + } + else if (chainsNotEnabled.size() == 0) // currently showing the required chains, only need to save new tokens + { + //store tokens + onSelectedChains(selected); + } + else // we should ask user to enable chains for selected tokens + { + showAddChainsDialog(); + } + } + + private void onSelectedChains(List selected) + { List toSave = new ArrayList<>(); for (TokenCardMeta tcm : selected) { Token matchingToken = tokenList.get(tcm.getChain()); if (matchingToken != null) toSave.add(matchingToken); } + viewModel.saveTokens(toSave); + onSaved(); + } - if (toSave.size() > 0) + private boolean requireMainNet() + { + boolean hasMainNetChain = false; + for (TokenCardMeta tcm : adapter.getSelected()) { - viewModel.saveTokens(toSave); - onSaved(toSave.get(0)); + if (viewModel.ethereumNetworkRepository().getNetworkByChain(tcm.getChain()).hasRealValue()) + { + hasMainNetChain = true; + break; + } } - else + + return hasMainNetChain; + } + + private void showAddChainsDialog() + { + if (dialog != null && dialog.isShowing()) { - finish(); + return; } + + dialog = new AWBottomSheetDialog(this, new AWBottomSheetDialog.Callback() + { + @Override + public void onClosed() + { + + } + + @Override + public void onConfirmed() + { + //switch to mainnet if required + if (!viewModel.ethereumNetworkRepository().isMainNetSelected() && requireMainNet()) + { + viewModel.setMainNetsSelected(true); // switch to mainnet before setting up filters + } + //add required chains + viewModel.selectExtraChains(getSelectedChains()); + //did we add the tokens? + onSelectedChains(adapter.getSelected()); + } + + @Override + public void onCancelled() + { + + } + }); + dialog.setTitle(getString(R.string.enable_required_chains)); + dialog.setContent(getString(R.string.enable_required_chains_message)); + dialog.setConfirmButton(getString(R.string.dialog_ok)); + dialog.show(); } private void onNoContractFound(Boolean noContract) @@ -440,11 +533,11 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { inputAddressView.setAddress(extracted_address); } break; - case QRScanner.DENY_PERMISSION: + case QRScannerActivity.DENY_PERMISSION: showCameraDenied(); break; default: - Timber.tag("SEND").e(String.format(getString(R.string.barcode_error_format), + Timber.tag("SEND").e(getString(R.string.barcode_error_format, "Code: " + resultCode )); break; @@ -478,4 +571,33 @@ public void onLongTokenClick(View view, Token token, List tokenIds) { } + + @Override + public void onTestNetDialogClosed() + { + + } + + @Override + public void onTestNetDialogConfirmed(long chainId) + { + //switch to testnet, ensuring the networks are selected + viewModel.setMainNetsSelected(false); + viewModel.selectExtraChains(getSelectedChains()); + //did we add the tokens? + onSelectedChains(adapter.getSelected()); + } + + private List getSelectedChains() + { + HashSet selectedChains = new HashSet<>(); + + for (TokenCardMeta token : adapter.getSelected()) + { + NetworkInfo info = viewModel.ethereumNetworkRepository().getNetworkByChain(token.getChain()); + selectedChains.add(info.chainId); + } + + return new ArrayList<>(selectedChains); + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/AdvancedSettingsActivity.java b/app/src/main/java/com/alphawallet/app/ui/AdvancedSettingsActivity.java index 2cb3ce2d2f..9cba1ec027 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AdvancedSettingsActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AdvancedSettingsActivity.java @@ -3,7 +3,6 @@ import static com.alphawallet.app.C.RESET_WALLET; import android.Manifest; -import android.app.AlertDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; @@ -35,7 +34,6 @@ public class AdvancedSettingsActivity extends BaseActivity { private AdvancedSettingsViewModel viewModel; - private SettingsItemView nodeStatus; private SettingsItemView console; private SettingsItemView clearBrowserCache; @@ -44,6 +42,8 @@ public class AdvancedSettingsActivity extends BaseActivity private SettingsItemView fullScreenSettings; private SettingsItemView refreshTokenDatabase; private SettingsItemView eip1559Transactions; + private SettingsItemView analytics; + private SettingsItemView crashReporting; private AWalletAlertDialog waitDialog = null; @Nullable @@ -54,7 +54,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this) - .get(AdvancedSettingsViewModel.class); + .get(AdvancedSettingsViewModel.class); setContentView(R.layout.activity_generic_settings); toolbar(); @@ -79,56 +79,68 @@ public void onDestroy() private void initializeSettings() { nodeStatus = new SettingsItemView.Builder(this) - .withIcon(R.drawable.ic_settings_node_status) - .withTitle(R.string.action_node_status) - .withListener(this::onNodeStatusClicked) - .build(); + .withIcon(R.drawable.ic_settings_node_status) + .withTitle(R.string.action_node_status) + .withListener(this::onNodeStatusClicked) + .build(); console = new SettingsItemView.Builder(this) - .withIcon(R.drawable.ic_settings_console) - .withTitle(R.string.title_console) - .withListener(this::onConsoleClicked) - .build(); + .withIcon(R.drawable.ic_settings_console) + .withTitle(R.string.title_console) + .withListener(this::onConsoleClicked) + .build(); clearBrowserCache = new SettingsItemView.Builder(this) - .withIcon(R.drawable.ic_settings_cache) - .withTitle(R.string.title_clear_browser_cache) - .withListener(this::onClearBrowserCacheClicked) - .build(); + .withIcon(R.drawable.ic_settings_cache) + .withTitle(R.string.title_clear_browser_cache) + .withListener(this::onClearBrowserCacheClicked) + .build(); tokenScript = new SettingsItemView.Builder(this) - .withIcon(R.drawable.ic_settings_tokenscript) - .withTitle(R.string.title_tokenscript) - .withListener(this::onTokenScriptClicked) - .build(); + .withIcon(R.drawable.ic_settings_tokenscript) + .withTitle(R.string.title_tokenscript) + .withListener(this::onTokenScriptClicked) + .build(); //TODO Change Icon tokenScriptManagement = new SettingsItemView.Builder(this) - .withIcon(R.drawable.ic_settings_tokenscript_manage) - .withTitle(R.string.tokenscript_management) - .withListener(this::onTokenScriptManagementClicked) - .build(); + .withIcon(R.drawable.ic_settings_tokenscript_manage) + .withTitle(R.string.tokenscript_management) + .withListener(this::onTokenScriptManagementClicked) + .build(); fullScreenSettings = new SettingsItemView.Builder(this) - .withType(SettingsItemView.Type.TOGGLE) - .withIcon(R.drawable.ic_phoneicon) - .withTitle(R.string.fullscreen) - .withListener(this::onFullScreenClicked) - .build(); + .withType(SettingsItemView.Type.TOGGLE) + .withIcon(R.drawable.ic_phoneicon) + .withTitle(R.string.fullscreen) + .withListener(this::onFullScreenClicked) + .build(); refreshTokenDatabase = new SettingsItemView.Builder(this) - .withIcon(R.drawable.ic_settings_reset_tokens) - .withTitle(R.string.title_reload_token_data) - .withListener(this::onReloadTokenDataClicked) - .build(); + .withIcon(R.drawable.ic_settings_reset_tokens) + .withTitle(R.string.title_reload_token_data) + .withListener(this::onReloadTokenDataClicked) + .build(); eip1559Transactions = new SettingsItemView.Builder(this) - .withType(SettingsItemView.Type.TOGGLE) - .withIcon(R.drawable.ic_icons_settings_1559) - .withTitle(R.string.experimental_1559) - .withSubtitle(R.string.experimental_1559_tx_sub) - .withListener(this::on1559TransactionsClicked) - .build(); + .withType(SettingsItemView.Type.TOGGLE) + .withIcon(R.drawable.ic_icons_settings_1559) + .withTitle(R.string.experimental_1559) + .withSubtitle(R.string.experimental_1559_tx_sub) + .withListener(this::on1559TransactionsClicked) + .build(); + + analytics = new SettingsItemView.Builder(this) + .withIcon(R.drawable.ic_settings_analytics) + .withTitle(R.string.settings_title_analytics) + .withListener(this::onAnalyticsClicked) + .build(); + + crashReporting = new SettingsItemView.Builder(this) + .withIcon(R.drawable.ic_settings_crash_reporting) + .withTitle(R.string.settings_title_crash_reporting) + .withListener(this::onCrashReportingClicked) + .build(); fullScreenSettings.setToggleState(viewModel.getFullScreenState()); eip1559Transactions.setToggleState(viewModel.get1559TransactionsState()); @@ -158,6 +170,8 @@ private void addSettingsToLayout() advancedSettingsLayout.addView(fullScreenSettings); advancedSettingsLayout.addView(refreshTokenDatabase); advancedSettingsLayout.addView(eip1559Transactions); + advancedSettingsLayout.addView(analytics); + advancedSettingsLayout.addView(crashReporting); } private void onNodeStatusClicked() @@ -177,16 +191,16 @@ private void onClearBrowserCacheClicked() viewModel.blankFilterSettings(); Single.fromCallable(() -> - { - Glide.get(this).clearDiskCache(); - return 1; - }).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(v -> - { - Toast.makeText(this, getString(R.string.toast_browser_cache_cleared), Toast.LENGTH_SHORT).show(); - finish(); - }).isDisposed(); + { + Glide.get(this).clearDiskCache(); + return 1; + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> + { + Toast.makeText(this, getString(R.string.toast_browser_cache_cleared), Toast.LENGTH_SHORT).show(); + finish(); + }).isDisposed(); } private void onReloadTokenDataClicked() @@ -197,7 +211,7 @@ private void onReloadTokenDataClicked() return; } - AWalletAlertDialog dialog = new AWalletAlertDialog(this); + AWalletAlertDialog dialog = new AWalletAlertDialog(this); dialog.setIcon(AWalletAlertDialog.NONE); dialog.setTitle(R.string.title_reload_token_data); dialog.setMessage(R.string.reload_token_data_desc); @@ -206,9 +220,9 @@ private void onReloadTokenDataClicked() viewModel.stopChainActivity(); showWaitDialog(); clearTokenCache = viewModel.resetTokenData() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::showResetResult); + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::showResetResult); viewModel.blankFilterSettings(); }); @@ -270,6 +284,18 @@ private void onTokenScriptManagementClicked() startActivity(intent); } + private void onCrashReportingClicked() + { + Intent intent = new Intent(this, CrashReportSettingsActivity.class); + startActivity(intent); + } + + private void onAnalyticsClicked() + { + Intent intent = new Intent(this, AnalyticsSettingsActivity.class); + startActivity(intent); + } + private void showXMLOverrideDialog() { AWalletConfirmationDialog cDialog = new AWalletConfirmationDialog(this); @@ -301,7 +327,7 @@ private void askWritePermission() private boolean checkWritePermission() { return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED; + == PackageManager.PERMISSION_GRANTED; } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/AnalyticsSettingsActivity.java b/app/src/main/java/com/alphawallet/app/ui/AnalyticsSettingsActivity.java new file mode 100644 index 0000000000..69a16caeac --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/AnalyticsSettingsActivity.java @@ -0,0 +1,76 @@ +package com.alphawallet.app.ui; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.R; +import com.alphawallet.app.viewmodel.AnalyticsSettingsViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.switchmaterial.SwitchMaterial; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class AnalyticsSettingsActivity extends BaseActivity +{ + AnalyticsSettingsViewModel viewModel; + SwitchMaterial analyticsSwitch; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_analytics_settings); + + toolbar(); + + setTitle(getString(R.string.settings_title_analytics)); + + viewModel = new ViewModelProvider(this).get(AnalyticsSettingsViewModel.class); + + initViews(); + } + + private void initViews() + { + analyticsSwitch = findViewById(R.id.switch_analytics); + analyticsSwitch.setChecked(viewModel.isAnalyticsEnabled()); + analyticsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewModel.toggleAnalytics(isChecked); + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { +// getMenuInflater().inflate(R.menu.menu_help, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == R.id.action_help) + { + showHelpUi(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void showHelpUi() + { + BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(this); + bottomSheetDialog.setCancelable(true); + bottomSheetDialog.setCanceledOnTouchOutside(true); +// bottomSheetDialog.setContentView(contentView); +// BottomSheetBehavior behavior = BottomSheetBehavior.from((View) contentView.getParent()); +// bottomSheetDialog.setOnShowListener(dialog -> behavior.setPeekHeight(contentView.getHeight())); + bottomSheetDialog.show(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java b/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java index 4fcbddb0a0..5c718a76b6 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java @@ -1,5 +1,8 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.C.Key.WALLET; +import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; + import android.content.Intent; import android.os.Bundle; import android.os.Handler; @@ -42,21 +45,17 @@ import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.token.entity.TSAction; import com.alphawallet.token.entity.TicketRange; +import com.alphawallet.token.entity.ViewType; import com.alphawallet.token.entity.XMLDsigDescriptor; import java.math.BigInteger; import java.util.List; import java.util.Map; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -import static com.alphawallet.app.C.Key.WALLET; -import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; - /** * Created by James on 22/01/2018. */ @@ -210,7 +209,7 @@ private void initWebViewCheck() { BigInteger tokenId = token.getArrayBalance().get(0); TicketRange data = new TicketRange(tokenId, token.getAddress()); - testView.renderTokenscriptView(token, data, viewModel.getAssetDefinitionService(), true); + testView.renderTokenscriptView(token, data, viewModel.getAssetDefinitionService(), ViewType.ITEM_VIEW); testView.setOnReadyCallback(this); } else @@ -441,7 +440,7 @@ private void displayTokens() { handler.removeCallbacks(this); progressView.setVisibility(View.GONE); - adapter = new NonFungibleTokenAdapter(functionBar, token, viewModel.getAssetDefinitionService(), viewModel.getOpenseaService(), this); + adapter = new NonFungibleTokenAdapter(functionBar, token, viewModel.getAssetDefinitionService(), viewModel.getOpenseaService()); functionBar.setupFunctions(this, viewModel.getAssetDefinitionService(), token, adapter, token.getArrayBalance()); functionBar.setWalletType(wallet.type); tokenView.setAdapter(adapter); diff --git a/app/src/main/java/com/alphawallet/app/ui/BaseActivity.java b/app/src/main/java/com/alphawallet/app/ui/BaseActivity.java index 20030baeba..5e91a90bef 100644 --- a/app/src/main/java/com/alphawallet/app/ui/BaseActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/BaseActivity.java @@ -1,5 +1,6 @@ package com.alphawallet.app.ui; +import android.content.Intent; import android.view.MenuItem; import android.widget.TextView; import android.widget.Toast; @@ -10,13 +11,23 @@ import androidx.appcompat.widget.Toolbar; import com.alphawallet.app.R; +import com.alphawallet.app.entity.AuthenticationCallback; +import com.alphawallet.app.entity.AuthenticationFailType; +import com.alphawallet.app.entity.Operation; import com.alphawallet.app.viewmodel.BaseViewModel; +import com.alphawallet.app.widget.SignTransactionDialog; -public abstract class BaseActivity extends AppCompatActivity { +public abstract class BaseActivity extends AppCompatActivity +{ + public static AuthenticationCallback authCallback; // Note: This static is only for signing callbacks + // which won't occur between wallet sessions - do not repeat this pattern + // for other code - protected Toolbar toolbar() { + protected Toolbar toolbar() + { Toolbar toolbar = findViewById(R.id.toolbar); - if (toolbar != null) { + if (toolbar != null) + { setSupportActionBar(toolbar); toolbar.setTitle(R.string.empty); } @@ -28,79 +39,127 @@ protected void setTitle(String title) { ActionBar actionBar = getSupportActionBar(); TextView toolbarTitle = findViewById(R.id.toolbar_title); - if (toolbarTitle != null) { - if (actionBar != null) { + if (toolbarTitle != null) + { + if (actionBar != null) + { actionBar.setTitle(R.string.empty); } toolbarTitle.setText(title); } } - protected void setSubtitle(String subtitle) { + protected void setSubtitle(String subtitle) + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.setSubtitle(subtitle); } } - protected void enableDisplayHomeAsUp() { + protected void enableDisplayHomeAsUp() + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.setDisplayHomeAsUpEnabled(true); } } - protected void enableDisplayHomeAsUp(@DrawableRes int resourceId) { + protected void enableDisplayHomeAsUp(@DrawableRes int resourceId) + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(resourceId); } } - protected void enableDisplayHomeAsHome(boolean active) { + protected void enableDisplayHomeAsHome(boolean active) + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.setDisplayHomeAsUpEnabled(active); actionBar.setHomeAsUpIndicator(R.drawable.ic_browser_home); } } - protected void dissableDisplayHomeAsUp() { + protected void dissableDisplayHomeAsUp() + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.setDisplayHomeAsUpEnabled(false); } } - protected void hideToolbar() { + protected void hideToolbar() + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.hide(); } } - protected void showToolbar() { + protected void showToolbar() + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null) + { actionBar.show(); } } @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - break; + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == android.R.id.home) + { + onBackPressed(); + finish(); } return true; } - public void displayToast(String message) { - if (message != null) { + public void displayToast(String message) + { + if (message != null) + { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); BaseViewModel.onPushToast(null); } } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + + //Interpret the return code; if it's within the range of values possible to return from PIN confirmation then separate out + //the task code from the return value. We have to do it this way because there's no way to send a bundle across the PIN dialog + //and out through the PIN dialog's return back to here + if (authCallback == null) + { + return; + } + + if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) + { + Operation taskCode = Operation.values()[requestCode - SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS]; + if (resultCode == RESULT_OK) + { + authCallback.authenticatePass(taskCode); + } + else + { + authCallback.authenticateFail("", AuthenticationFailType.PIN_FAILED, taskCode); + } + + authCallback = null; + } + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/BaseFragment.java b/app/src/main/java/com/alphawallet/app/ui/BaseFragment.java index ba903731d8..c2c1a250f2 100644 --- a/app/src/main/java/com/alphawallet/app/ui/BaseFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/BaseFragment.java @@ -1,76 +1,101 @@ package com.alphawallet.app.ui; -import androidx.fragment.app.Fragment; -import androidx.appcompat.widget.Toolbar; +import android.content.Intent; import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; + import com.alphawallet.app.R; +import com.alphawallet.app.entity.BackupTokenCallback; +import com.alphawallet.app.entity.ContractLocator; +import com.alphawallet.app.entity.FragmentMessenger; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint -public class BaseFragment extends Fragment implements Toolbar.OnMenuItemClickListener { +public class BaseFragment extends Fragment implements Toolbar.OnMenuItemClickListener, + BackupTokenCallback +{ private Toolbar toolbar; private TextView toolbarTitle; - private void initToolbar(View view) { + private void initToolbar(View view) + { toolbar = view.findViewById(R.id.toolbar); toolbarTitle = toolbar.findViewById(R.id.toolbar_title); } - protected void toolbar(View view) { + + protected void toolbar(View view) + { if (view != null) initToolbar(view); } - protected void toolbar(View view, int title, int menuResId) { + protected void toolbar(View view, int title, int menuResId) + { initToolbar(view); setToolbarTitle(title); setToolbarMenu(menuResId); } - protected void toolbar(View view, int menuResId) { + protected void toolbar(View view, int menuResId) + { initToolbar(view); setToolbarMenu(menuResId); } - protected void toolbar(View view, int menuResId, Toolbar.OnMenuItemClickListener listener) { + protected void toolbar(View view, int menuResId, Toolbar.OnMenuItemClickListener listener) + { initToolbar(view); setToolbarMenu(menuResId); setToolbarMenuItemClickListener(listener); } - protected void toolbar(View view, int title, int menuResId, Toolbar.OnMenuItemClickListener listener) { + protected void toolbar(View view, int title, int menuResId, Toolbar.OnMenuItemClickListener listener) + { initToolbar(view); setToolbarTitle(title); setToolbarMenu(menuResId); setToolbarMenuItemClickListener(listener); } - protected void setToolbarTitle(String title) { - if (toolbarTitle != null) { + protected void setToolbarTitle(String title) + { + if (toolbarTitle != null) + { toolbarTitle.setText(title); } } - protected void setToolbarMenuItemClickListener(Toolbar.OnMenuItemClickListener listener) { + protected void setToolbarMenuItemClickListener(Toolbar.OnMenuItemClickListener listener) + { toolbar.setOnMenuItemClickListener(listener); } - protected void setToolbarTitle(int title) { + protected void setToolbarTitle(int title) + { setToolbarTitle(getString(title)); } - protected void setToolbarMenu(int menuRes) { + protected void setToolbarMenu(int menuRes) + { toolbar.inflateMenu(menuRes); } - public Toolbar getToolbar() { + public Toolbar getToolbar() + { return toolbar; } @Override - public boolean onMenuItemClick(MenuItem menuItem) { + public boolean onMenuItemClick(MenuItem menuItem) + { return false; } @@ -84,6 +109,79 @@ public void leaveFocus() // } - public void softKeyboardVisible() { } - public void softKeyboardGone() { } + public void softKeyboardVisible() + { + } + + public void softKeyboardGone() + { + } + + public void onItemClick(String url) + { + } + + public void signalPlayStoreUpdate(int updateVersion) + { + } + + public void signalExternalUpdate(String updateVersion) + { + } + + public void backupSeedSuccess(boolean hasNoLock) + { + } + + public void storeWalletBackupTime(String backedUpKey) + { + } + + public void resetTokens() + { + } + + public void resetTransactions() + { + } + + public void gotCameraAccess(@NotNull String[] permissions, int[] grantResults) + { + } + + public void gotGeoAccess(@NotNull String[] permissions, int[] grantResults) + { + } + + public void gotFileAccess(@NotNull String[] permissions, int[] grantResults) + { + } + + public void handleQRCode(int resultCode, Intent data, FragmentMessenger messenger) + { + } + + public void pinAuthorisation(boolean gotAuth) + { + } + + public void switchNetworkAndLoadUrl(long chainId, String url) + { + } + + public void scrollToTop() + { + } + + public void addedToken(List tokenContracts) + { + } + + public void setImportFilename(String fName) + { + } + + public void backPressed() + { + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/BrowserHistoryFragment.java b/app/src/main/java/com/alphawallet/app/ui/BrowserHistoryFragment.java index 1d39b139c1..7a630353d5 100644 --- a/app/src/main/java/com/alphawallet/app/ui/BrowserHistoryFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/BrowserHistoryFragment.java @@ -1,5 +1,8 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.ui.DappBrowserFragment.DAPP_CLICK; +import static com.alphawallet.app.ui.DappBrowserFragment.DAPP_REMOVE_HISTORY; + import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -8,26 +11,27 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.DApp; import com.alphawallet.app.ui.widget.OnDappClickListener; import com.alphawallet.app.ui.widget.adapter.BrowserHistoryAdapter; import com.alphawallet.app.util.DappBrowserUtils; +import com.alphawallet.app.viewmodel.BrowserHistoryViewModel; import com.alphawallet.app.widget.AWalletAlertDialog; import java.util.List; -import static com.alphawallet.app.ui.DappBrowserFragment.DAPP_CLICK; -import static com.alphawallet.app.ui.DappBrowserFragment.DAPP_REMOVE_HISTORY; - import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint -public class BrowserHistoryFragment extends Fragment { +public class BrowserHistoryFragment extends BaseFragment +{ + private BrowserHistoryViewModel viewModel; private BrowserHistoryAdapter adapter; private AWalletAlertDialog dialog; private TextView clear; @@ -36,7 +40,8 @@ public class BrowserHistoryFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + @Nullable Bundle savedInstanceState) + { View view = inflater.inflate(R.layout.layout_browser_history, container, false); adapter = new BrowserHistoryAdapter( getData(), @@ -62,11 +67,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c dialog.setSecondaryButtonText(R.string.dialog_cancel_back); dialog.show(); }); - + viewModel = new ViewModelProvider(this).get(BrowserHistoryViewModel.class); showOrHideViews(); return view; } + @Override + public void onResume() + { + super.onResume(); + viewModel.track(Analytics.Navigation.BROWSER_HISTORY); + } + @Override public void onDetach() { @@ -74,23 +86,29 @@ public void onDetach() adapter.clear(); } - private void showOrHideViews() { - if (adapter.getItemCount() > 0) { + private void showOrHideViews() + { + if (adapter.getItemCount() > 0) + { clear.setVisibility(View.VISIBLE); noHistory.setVisibility(View.GONE); - } else { + } + else + { clear.setVisibility(View.GONE); noHistory.setVisibility(View.VISIBLE); } } - private void clearHistory() { + private void clearHistory() + { DappBrowserUtils.clearHistory(getContext()); adapter.setDapps(getData()); showOrHideViews(); } - private void onHistoryItemRemoved(DApp dapp) { + private void onHistoryItemRemoved(DApp dapp) + { DappBrowserUtils.removeFromHistory(getContext(), dapp); adapter.setDapps(getData()); showOrHideViews(); @@ -104,7 +122,8 @@ private void setFragmentResult(String key, DApp dapp) getParentFragmentManager().setFragmentResult(DAPP_CLICK, result); } - private List getData() { + private List getData() + { return DappBrowserUtils.getBrowserHistory(getContext()); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/CoinbasePayActivity.java b/app/src/main/java/com/alphawallet/app/ui/CoinbasePayActivity.java new file mode 100644 index 0000000000..4780bacaad --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/CoinbasePayActivity.java @@ -0,0 +1,104 @@ +package com.alphawallet.app.ui; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.text.TextUtils; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.coinbasepay.DestinationWallet; +import com.alphawallet.app.viewmodel.CoinbasePayViewModel; + +import java.util.ArrayList; +import java.util.List; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CoinbasePayActivity extends BaseActivity +{ + private CoinbasePayViewModel viewModel; + private WebView webView; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_coinbase_pay); + + toolbar(); + + setTitle(getString(R.string.title_buy_with_coinbase_pay)); + + initViewModel(); + + initWebView(); + + viewModel.track(Analytics.Navigation.COINBASE_PAY); + + viewModel.prepare(); + } + + @SuppressLint("SetJavaScriptEnabled") + private void initWebView() + { + webView = findViewById(R.id.web_view); + webView.setWebViewClient(new WebViewClient()); + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); + webView.getSettings().setAppCacheEnabled(false); + webView.clearCache(true); + webView.clearHistory(); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this).get(CoinbasePayViewModel.class); + viewModel.defaultWallet().observe(this, this::onDefaultWallet); + } + + private void onDefaultWallet(Wallet wallet) + { + DestinationWallet.Type type; + List list = new ArrayList<>(); + String asset = getIntent().getStringExtra("asset"); + if (!TextUtils.isEmpty(asset)) + { + type = DestinationWallet.Type.ASSETS; + list.add(asset); + } + else + { + type = DestinationWallet.Type.BLOCKCHAINS; + String blockchain = getIntent().getStringExtra("blockchain"); + list.add(blockchain); + } + + String uri = viewModel.getUri(type, wallet.address, list); + if (TextUtils.isEmpty(uri)) + { + Toast.makeText(this, "Missing Coinbase Pay App ID.", Toast.LENGTH_LONG).show(); + finish(); + } + else + { + webView.loadUrl(uri); + } + } + + @Override + public void onBackPressed() + { + webView.clearCache(true); + super.onBackPressed(); + overridePendingTransition(R.anim.hold, R.anim.slide_out_right); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/CrashReportSettingsActivity.java b/app/src/main/java/com/alphawallet/app/ui/CrashReportSettingsActivity.java new file mode 100644 index 0000000000..c9d35759cd --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/CrashReportSettingsActivity.java @@ -0,0 +1,76 @@ +package com.alphawallet.app.ui; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.R; +import com.alphawallet.app.viewmodel.AnalyticsSettingsViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.switchmaterial.SwitchMaterial; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CrashReportSettingsActivity extends BaseActivity +{ + AnalyticsSettingsViewModel viewModel; + SwitchMaterial crashReportSwitch; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_crash_report_settings); + + toolbar(); + + setTitle(getString(R.string.settings_title_crash_reporting)); + + viewModel = new ViewModelProvider(this).get(AnalyticsSettingsViewModel.class); + + initViews(); + } + + private void initViews() + { + crashReportSwitch = findViewById(R.id.switch_crash_report); + crashReportSwitch.setChecked(viewModel.isCrashReportingEnabled()); + crashReportSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewModel.toggleCrashReporting(isChecked); + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { +// getMenuInflater().inflate(R.menu.menu_help, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == R.id.action_help) + { + showHelpUi(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void showHelpUi() + { + BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(this); + bottomSheetDialog.setCancelable(true); + bottomSheetDialog.setCanceledOnTouchOutside(true); +// bottomSheetDialog.setContentView(contentView); +// BottomSheetBehavior behavior = BottomSheetBehavior.from((View) contentView.getParent()); +// bottomSheetDialog.setOnShowListener(dialog -> behavior.setPeekHeight(contentView.getHeight())); + bottomSheetDialog.show(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java b/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java index 17eb20dbb0..9be1d8ec45 100644 --- a/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java @@ -2,21 +2,17 @@ import static com.alphawallet.app.C.ETHER_DECIMALS; import static com.alphawallet.app.C.RESET_TOOLBAR; -import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; -import static com.alphawallet.app.entity.Operation.SIGN_DATA; import static com.alphawallet.app.entity.tokens.Token.TOKEN_BALANCE_PRECISION; import static com.alphawallet.app.ui.HomeActivity.RESET_TOKEN_SERVICE; import static com.alphawallet.app.ui.MyAddressActivity.KEY_ADDRESS; -import static com.alphawallet.app.util.KeyboardUtils.showKeyboard; import static com.alphawallet.app.util.Utils.isValidUrl; import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; +import static com.alphawallet.token.entity.MagicLinkInfo.mainnetMagicLinkDomain; import static org.web3j.protocol.core.methods.request.Transaction.createFunctionCallTransaction; import android.Manifest; -import android.animation.Animator; import android.animation.LayoutTransition; -import android.animation.ValueAnimator; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ClipData; @@ -29,15 +25,12 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; @@ -47,7 +40,6 @@ import android.webkit.WebHistoryItem; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.AutoCompleteTextView; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; @@ -67,10 +59,11 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.DApp; -import com.alphawallet.app.entity.DAppFunction; import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.QRResult; @@ -79,24 +72,23 @@ import com.alphawallet.app.entity.URLLoadInterface; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletConnectActions; -import com.alphawallet.app.entity.WalletPage; import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.analytics.ActionSheetSource; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.repository.entity.RealmToken; import com.alphawallet.app.service.WalletConnectService; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.OnDappHomeNavClickListener; -import com.alphawallet.app.ui.widget.adapter.DappBrowserSuggestionsAdapter; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; import com.alphawallet.app.ui.widget.entity.DappBrowserSwipeInterface; import com.alphawallet.app.ui.widget.entity.DappBrowserSwipeLayout; import com.alphawallet.app.ui.widget.entity.ItemClickListener; import com.alphawallet.app.util.BalanceUtils; import com.alphawallet.app.util.DappBrowserUtils; -import com.alphawallet.app.util.KeyboardUtils; import com.alphawallet.app.util.LocaleUtils; import com.alphawallet.app.util.QRParser; import com.alphawallet.app.util.Utils; @@ -114,7 +106,11 @@ import com.alphawallet.app.web3.entity.Web3Call; import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.widget.ActionSheet; import com.alphawallet.app.widget.ActionSheetDialog; +import com.alphawallet.app.widget.ActionSheetSignDialog; +import com.alphawallet.app.widget.AddressBar; +import com.alphawallet.app.widget.AddressBarListener; import com.alphawallet.app.widget.TestNetDialog; import com.alphawallet.token.entity.EthereumMessage; import com.alphawallet.token.entity.EthereumTypedMessage; @@ -126,8 +122,6 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import org.web3j.crypto.Keys; -import org.web3j.crypto.Sign; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.methods.response.EthCall; @@ -138,16 +132,15 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; -import java.security.SignatureException; -import java.util.concurrent.TimeUnit; +import java.net.URLDecoder; +import java.nio.charset.Charset; import dagger.hilt.android.AndroidEntryPoint; -import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; import io.realm.RealmResults; @@ -157,7 +150,8 @@ public class DappBrowserFragment extends BaseFragment implements OnSignTransactionListener, OnSignPersonalMessageListener, OnSignTypedMessageListener, OnSignMessageListener, OnEthCallListener, OnWalletAddEthereumChainObjectListener, OnWalletActionListener, URLLoadInterface, ItemClickListener, OnDappHomeNavClickListener, DappBrowserSwipeInterface, - SignAuthenticationCallback, ActionSheetCallback, TestNetDialog.TestNetDialogCallback { + SignAuthenticationCallback, ActionSheetCallback, TestNetDialog.TestNetDialogCallback +{ public static final String SEARCH = "SEARCH"; public static final String PERSONAL_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"; public static final String CURRENT_FRAGMENT = "currentFragment"; @@ -179,11 +173,11 @@ public class DappBrowserFragment extends BaseFragment implements OnSignTransacti /** * Below object is used to set Animation duration for expand/collapse and rotate */ - private final int ANIMATION_DURATION = 100; private final Handler handler = new Handler(Looper.getMainLooper()); private ValueCallback uploadMessage; ActivityResultLauncher getContent = registerForActivityResult(new ActivityResultContracts.GetContent(), - new ActivityResultCallback() { + new ActivityResultCallback() + { @Override public void onActivityResult(Uri uri) { @@ -193,31 +187,27 @@ public void onActivityResult(Uri uri) private WebChromeClient.FileChooserParams fileChooserParams; private RealmResults realmUpdate; private Realm realm = null; - private ActionSheetDialog confirmationDialog; + private ActionSheet confirmationDialog; ActivityResultLauncher getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> confirmationDialog.setCurrentGasIndex(result)); private DappBrowserViewModel viewModel; private DappBrowserSwipeLayout swipeRefreshLayout; private Web3View web3; - private AutoCompleteTextView urlTv; private ProgressBar progressBar; private Wallet wallet; private NetworkInfo activeNetwork; private AWalletAlertDialog chainSwapDialog; private AWalletAlertDialog resultDialog; - private DappBrowserSuggestionsAdapter adapter; private String loadOnInit; //Web3 needs to be fully set up and initialised before any dapp loading can be done private boolean homePressed; private AddEthereumChainPrompt addCustomChainDialog; private Toolbar toolbar; - private ImageView back; - private ImageView next; - private ImageView clear; private ImageView refresh; private FrameLayout webFrame; private TextView balance; private TextView symbol; - private View layoutNavigation; + private AddressBar addressBar; + // Handle resizing the browser view when the soft keyboard pops up and goes. // The issue this fixes is where you need to enter data at the bottom of the webpage, // and the keyboard hides the input field @@ -250,7 +240,8 @@ else if (heightDifference == 0 && layoutParams.bottomMargin != navBarHeight) //go back into full screen mode, and expand URL bar out layoutParams.bottomMargin = 0; webFrame.setLayoutParams(layoutParams); - shrinkSearchBar(); + toolbar.getMenu().setGroupVisible(R.id.dapp_browser_menu, true); + addressBar.shrinkSearchBar(); } return insets; @@ -259,7 +250,6 @@ else if (heightDifference == 0 && layoutParams.bottomMargin != navBarHeight) private PermissionRequest requestCallback = null; private String geoOrigin; private String walletConnectSession; - private boolean focusFlag; private String currentWebpageTitle; private String currentFragment; ActivityResultLauncher getNetwork = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), @@ -281,16 +271,12 @@ else if (heightDifference == 0 && layoutParams.bottomMargin != navBarHeight) // Some multi-chain Dapps have a watchdog thread that checks the chain // This thread stays in operation until a new page load is complete. private String loadUrlAfterReload; - private DAppFunction dAppFunction; - @Nullable - private Disposable disposable; @Override public void onCreate(@Nullable Bundle savedInstanceState) { LocaleUtils.setActiveLocale(getContext()); super.onCreate(savedInstanceState); - focusFlag = false; getChildFragmentManager() .setFragmentResultListener(DAPP_CLICK, this, (requestKey, bundle) -> { @@ -303,7 +289,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) } else if (removedDapp != null) { - adapter.removeSuggestion(removedDapp); + addressBar.removeSuggestion(removedDapp); } }); } @@ -315,12 +301,14 @@ public void onResume() homePressed = false; if (currentFragment == null) currentFragment = DAPP_BROWSER; attachFragment(currentFragment); - if ((web3 == null || viewModel == null) && getActivity() != null) //trigger reload + if ((web3 == null || viewModel == null)) //trigger reload { - ((HomeActivity) getActivity()).resetFragment(WalletPage.DAPP_BROWSER); + //reboot + requireActivity().recreate(); } else { + viewModel.track(Analytics.Navigation.BROWSER); web3.setWebLoadCallback(this); } @@ -337,7 +325,46 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c View view = inflater.inflate(webViewID, container, false); initViewModel(); initView(view); - setupAddressBar(); + + addressBar.setup(viewModel.getDappsMasterList(getContext()), new AddressBarListener() + { + @Override + public boolean onLoad(String urlText) + { + addToBackStack(DAPP_BROWSER); + boolean handled = loadUrl(urlText); + detachFragments(); + cancelSearchSession(); + return handled; + } + + @Override + public void onClear() + { + cancelSearchSession(); + } + + @Override + public WebBackForwardList loadNext() + { + goToNextPage(); + return web3.copyBackForwardList(); + } + + @Override + public WebBackForwardList loadPrevious() + { + backPressed(); + return web3.copyBackForwardList(); + } + + @Override + public WebBackForwardList onHomePagePressed() + { + homePressed(); + return web3.copyBackForwardList(); + } + }); attachFragment(DAPP_BROWSER); @@ -382,7 +409,7 @@ private void showFragment(Fragment fragment, String tag) .add(R.id.frame, fragment, tag) .commit(); - setBackForwardButtons(); + addressBar.updateNavigationButtons(web3.copyBackForwardList()); } private void detachFragments() @@ -398,15 +425,11 @@ private void homePressed() homePressed = true; detachFragments(); currentFragment = DAPP_BROWSER; - if (urlTv != null) - urlTv.getText().clear(); + addressBar.clear(); if (web3 != null) { resetDappBrowser(); } - - //blank forward / backward arrows - setBackForwardButtons(); } @Override @@ -422,7 +445,7 @@ public void onDestroy() super.onDestroy(); viewModel.onDestroy(); stopBalanceListener(); - if (disposable != null && !disposable.isDisposed()) disposable.dispose(); + addressBar.destroy(); } private void setupMenu(@NotNull View baseView) @@ -458,7 +481,7 @@ private void setupMenu(@NotNull View baseView) return true; }); if (add != null) add.setOnMenuItemClickListener(menuItem -> { - viewModel.addToMyDapps(getContext(), currentWebpageTitle, urlTv.getText().toString()); + viewModel.addToMyDapps(getContext(), currentWebpageTitle, addressBar.getUrl()); return true; }); if (history != null) history.setOnMenuItemClickListener(menuItem -> { @@ -487,7 +510,7 @@ private void setupMenu(@NotNull View baseView) if (setAsHomePage != null) { setAsHomePage.setOnMenuItemClickListener(menuItem -> { - viewModel.setHomePage(getContext(), urlTv.getText().toString()); + viewModel.setHomePage(getContext(), addressBar.getUrl()); return true; }); } @@ -505,7 +528,6 @@ private void updateNetworkMenuItem() private void initView(@NotNull View view) { web3 = view.findViewById(R.id.web3view); - urlTv = view.findViewById(R.id.url_tv); Bundle savedState = readBundleFromLocal(); if (savedState != null) { @@ -518,17 +540,13 @@ private void initView(@NotNull View view) loadOnInit = getDefaultDappUrl(); } + addressBar = view.findViewById(R.id.address_bar_widget); progressBar = view.findViewById(R.id.progressBar); - urlTv = view.findViewById(R.id.url_tv); webFrame = view.findViewById(R.id.frame); swipeRefreshLayout = view.findViewById(R.id.swipe_refresh); swipeRefreshLayout.setRefreshInterface(this); toolbar = view.findViewById(R.id.address_bar); - layoutNavigation = view.findViewById(R.id.layout_navigator); - - View home = view.findViewById(R.id.home); - if (home != null) home.setOnClickListener(v -> homePressed()); //If you are wondering about the strange way the menus are inflated - this is required to ensure //that the menu text gets created with the correct localisation under every circumstance @@ -551,17 +569,6 @@ else if (getDefaultDappUrl() != null) refresh.setOnClickListener(v -> reloadPage()); } - back = view.findViewById(R.id.back); - back.setOnClickListener(v -> backPressed()); - - next = view.findViewById(R.id.next); - next.setOnClickListener(v -> goToNextPage()); - - clear = view.findViewById(R.id.clear_url); - clear.setOnClickListener(v -> { - clearAddressBar(); - }); - balance = view.findViewById(R.id.balance); symbol = view.findViewById(R.id.symbol); web3.setWebLoadCallback(this); @@ -593,85 +600,12 @@ private void openNetworkSelection() getNetwork.launch(intent); } - private void clearAddressBar() - { - if (urlTv.getText().toString().isEmpty()) - { - cancelSearchSession(); - } - else - { - urlTv.getText().clear(); - openURLInputView(); - KeyboardUtils.showKeyboard(urlTv); //ensure keyboard shows here so we can listen for it being cancelled - } - } - - private void setupAddressBar() - { - adapter = new DappBrowserSuggestionsAdapter( - requireContext(), - viewModel.getDappsMasterList(getContext()), - this::onItemClick - ); - urlTv.setAdapter(null); - - urlTv.setOnEditorActionListener((v, actionId, event) -> { - boolean handled = false; - if (actionId == EditorInfo.IME_ACTION_GO) - { - String urlText = urlTv.getText().toString(); - handled = loadUrl(urlText); - detachFragments(); - cancelSearchSession(); - } - return handled; - }); - - // Both these are required, the onFocus listener is required to respond to the first click. - urlTv.setOnFocusChangeListener((v, hasFocus) -> { - //see if we have focus flag - if (hasFocus && focusFlag && getActivity() != null) openURLInputView(); - }); - - urlTv.setOnClickListener(v -> { - openURLInputView(); - }); - - urlTv.setShowSoftInputOnFocus(true); - - urlTv.setOnLongClickListener(v -> { - urlTv.dismissDropDown(); - return false; - }); - - urlTv.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) - { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) - { - - } - - @Override - public void afterTextChanged(Editable editable) - { - adapter.setHighlighted(editable.toString()); - } - }); - } - @Override public void comeIntoFocus() { if (viewModel != null) { - if (viewModel.getActiveNetwork() == null || activeNetwork.chainId != viewModel.getActiveNetwork().chainId) + if (viewModel.getActiveNetwork() == null || activeNetwork == null || activeNetwork.chainId != viewModel.getActiveNetwork().chainId) { viewModel.checkForNetworkChanges(); } @@ -682,121 +616,21 @@ public void comeIntoFocus() viewModel.updateGasPrice(activeNetwork.chainId); } } - if (urlTv != null) - { - urlTv.clearFocus(); - KeyboardUtils.hideKeyboard(urlTv); - } - focusFlag = true; + addressBar.leaveEditMode(); } @Override public void leaveFocus() { - focusFlag = false; if (web3 != null) web3.requestFocus(); - if (urlTv != null) urlTv.clearFocus(); + addressBar.leaveFocus(); if (viewModel != null) viewModel.stopBalanceUpdate(); stopBalanceListener(); } - // TODO: Move all nav stuff to widget - private void openURLInputView() - { - urlTv.setAdapter(null); - expandCollapseView(layoutNavigation, false); - - disposable = Observable.zip( - Observable.interval(600, TimeUnit.MILLISECONDS).take(1), - Observable.fromArray(clear), (interval, item) -> item) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(AndroidSchedulers.mainThread()) - .subscribe(this::postBeginSearchSession); - } - - private void postBeginSearchSession(@NotNull ImageView item) - { - urlTv.setAdapter(adapter); - urlTv.showDropDown(); - if (item.getVisibility() == View.GONE) - { - expandCollapseView(item, true); - showKeyboard(urlTv); - } - } - /** * Used to expand or collapse the view */ - private synchronized void expandCollapseView(@NotNull View view, boolean expandView) - { - //detect if view is expanded or collapsed - boolean isViewExpanded = view.getVisibility() == View.VISIBLE; - - //Collapse view - if (isViewExpanded && !expandView) - { - int finalWidth = view.getWidth(); - ValueAnimator valueAnimator = slideAnimator(finalWidth, 0, view); - valueAnimator.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animator) - { - - } - - @Override - public void onAnimationEnd(Animator animator) - { - view.setVisibility(View.GONE); - } - - @Override - public void onAnimationCancel(Animator animator) - { - - } - - @Override - public void onAnimationRepeat(Animator animator) - { - - } - }); - valueAnimator.start(); - } - //Expand view - else if (!isViewExpanded && expandView) - { - view.setVisibility(View.VISIBLE); - - int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - - view.measure(widthSpec, heightSpec); - int width = view.getMeasuredWidth(); - ValueAnimator valueAnimator = slideAnimator(0, width, view); - valueAnimator.start(); - } - } - - @NotNull - private ValueAnimator slideAnimator(int start, int end, final View view) - { - - final ValueAnimator animator = ValueAnimator.ofInt(start, end); - - animator.addUpdateListener(valueAnimator -> { - // Update Height - int value = (Integer) valueAnimator.getAnimatedValue(); - - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - layoutParams.width = value; - view.setLayoutParams(layoutParams); - }); - animator.setDuration(ANIMATION_DURATION); - return animator; - } private void addToBackStack(String nextFragment) { @@ -815,19 +649,7 @@ private void addToForwardStack(String prevFragment) private void cancelSearchSession() { detachFragment(SEARCH); - KeyboardUtils.hideKeyboard(urlTv); - setBackForwardButtons(); - } - - private void shrinkSearchBar() - { - if (toolbar != null) - { - toolbar.getMenu().setGroupVisible(R.id.dapp_browser_menu, true); - expandCollapseView(layoutNavigation, true); - clear.setVisibility(View.GONE); - urlTv.dismissDropDown(); - } + addressBar.updateNavigationButtons(web3.copyBackForwardList()); } private void detachFragment(String tag) @@ -872,7 +694,7 @@ private void startBalanceListener() symbol.setVisibility(View.VISIBLE); String newBalanceStr = BalanceUtils.getScaledValueFixed(new BigDecimal(realmToken.getBalance()), ETHER_DECIMALS, TOKEN_BALANCE_PRECISION); balance.setText(newBalanceStr); - symbol.setText(activeNetwork.getShortName()); + symbol.setText(activeNetwork != null ? activeNetwork.getShortName() : ""); }); } @@ -898,6 +720,7 @@ private void onDefaultWallet(Wallet wallet) } } + @Override public void switchNetworkAndLoadUrl(long chainId, String url) { forceChainChange = chainId; //avoid prompt to change chain for 1inch @@ -930,7 +753,7 @@ private void onNetworkChanged(NetworkInfo networkInfo) updateNetworkMenuItem(); } - if (networkChanged && isOnHomePage()) + if (networkChanged && addressBar.isOnHomePage()) resetDappBrowser(); //trigger a reset if on homepage updateFilters(networkInfo); @@ -996,7 +819,8 @@ private void setupWeb3() web3.setRpcUrl(viewModel.getNetworkNodeRPC(activeNetwork.chainId)); web3.setWalletAddress(new Address(wallet.address)); - web3.setWebChromeClient(new WebChromeClient() { + web3.setWebChromeClient(new WebChromeClient() + { @Override public void onProgressChanged(WebView webview, int newProgress) { @@ -1066,7 +890,8 @@ public boolean onShowFileChooser(WebView webView, ValueCallback filePathC } }); - web3.setWebViewClient(new WebViewClient() { + web3.setWebViewClient(new WebViewClient() + { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { @@ -1111,6 +936,21 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) } } + if (fromWalletConnectModal(url)) + { + String encodedURL = url.split("=")[1]; + try + { + String decodedURL = URLDecoder.decode(encodedURL, Charset.defaultCharset().name()); + viewModel.handleWalletConnect(getContext(), decodedURL, activeNetwork); + return true; + } + catch (UnsupportedEncodingException e) + { + Timber.d("Decode URL failed: " + e); + } + } + setUrlText(url); return false; } @@ -1135,15 +975,15 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) } } + private boolean fromWalletConnectModal(String url) + { + return url.startsWith("https://" + mainnetMagicLinkDomain + "/wc?uri="); + } + private void setUrlText(String newUrl) { - if (urlTv == null) - { - if (getView() == null) return; //unable to get view at this time - urlTv = getView().findViewById(R.id.url_tv); - } - urlTv.setText(newUrl); - setBackForwardButtons(); + addressBar.setUrl(newUrl); + addressBar.updateNavigationButtons(web3.copyBackForwardList()); } private void loadNewNetwork(long newNetworkId) @@ -1177,14 +1017,6 @@ protected boolean requestUpload() return true; } - public void setCurrentGasIndex(int gasSelectionIndex, BigDecimal customGasPrice, BigDecimal customGasLimit, long expectedTxTime, long customNonce) - { - /*if (confirmationDialog != null && confirmationDialog.isShowing()) - { - confirmationDialog.setCurrentGasIndex(gasSelectionIndex, customGasPrice, customGasLimit, expectedTxTime, customNonce); - }*/ - } - @Override public void onSignMessage(final EthereumMessage message) { @@ -1214,13 +1046,13 @@ public void onSignTypedMessage(@NotNull EthereumTypedMessage message) public void onEthCall(Web3Call call) { Single.fromCallable(() -> { - //let's make the call - Web3j web3j = TokenRepository.getWeb3jService(activeNetwork.chainId); - //construct call - org.web3j.protocol.core.methods.request.Transaction transaction - = createFunctionCallTransaction(wallet.address, null, null, call.gasLimit, call.to.toString(), call.value, call.payload); - return web3j.ethCall(transaction, call.blockParam).send(); - }).map(EthCall::getValue) + //let's make the call + Web3j web3j = TokenRepository.getWeb3jService(activeNetwork.chainId); + //construct call + org.web3j.protocol.core.methods.request.Transaction transaction + = createFunctionCallTransaction(wallet.address, null, null, call.gasLimit, call.to.toString(), call.value, call.payload); + return web3j.ethCall(transaction, call.blockParam).send(); + }).map(EthCall::getValue) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> web3.onCallFunctionSuccessful(call.leafPosition, result), @@ -1245,8 +1077,14 @@ public void onWalletAddEthereumChainObject(long callbackId, WalletAddEthereumCha { // show add custom chain dialog addCustomChainDialog = new AddEthereumChainPrompt(getContext(), chainObj, chainObject -> { - viewModel.addCustomChain(chainObject); - loadNewNetwork(chainObj.getChainId()); + if (viewModel.addCustomChain(chainObject)) + { + loadNewNetwork(chainObj.getChainId()); + } + else + { + displayError(R.string.error_invalid_url, 0); + } addCustomChainDialog.dismiss(); }); addCustomChainDialog.show(); @@ -1340,34 +1178,29 @@ private void showChainChangeDialog(long callbackId, NetworkInfo newNetwork) private void handleSignMessage(Signable message) { - dAppFunction = new DAppFunction() { - @Override - public void DAppError(Throwable error, Signable message) - { - web3.onSignCancel(message.getCallbackId()); - confirmationDialog.dismiss(); - } - - @Override - public void DAppReturn(byte[] data, Signable message) - { - String signHex = Numeric.toHexString(data); - Timber.d("Initial Msg: %s", message.getMessage()); - web3.onSignMessageSuccessful(message, signHex); - - confirmationDialog.success(); - } - }; - if (confirmationDialog == null || !confirmationDialog.isShowing()) { - confirmationDialog = new ActionSheetDialog(requireActivity(), this, this, message); - confirmationDialog.setCanceledOnTouchOutside(false); + confirmationDialog = new ActionSheetSignDialog(requireActivity(), this, message); confirmationDialog.show(); - confirmationDialog.fullExpand(); } } + @Override + public void signingComplete(SignatureFromKey signature, Signable message) + { + String signHex = Numeric.toHexString(signature.signature); + Timber.d("Initial Msg: %s", message.getMessage()); + confirmationDialog.success(); + web3.onSignMessageSuccessful(message, signHex); + } + + @Override + public void signingFailed(Throwable error, Signable message) + { + web3.onSignCancel(message.getCallbackId()); + confirmationDialog.dismiss(); + } + @Override public void onSignTransaction(Web3Transaction transaction, String url) { @@ -1386,8 +1219,7 @@ public void onSignTransaction(Web3Transaction transaction, String url) confirmationDialog.show(); confirmationDialog.fullExpand(); - viewModel.calculateGasEstimate(wallet, Numeric.hexStringToByteArray(transaction.payload), - activeNetwork.chainId, transaction.recipient.toString(), new BigDecimal(transaction.value), transaction.gasLimit) + viewModel.calculateGasEstimate(wallet, transaction, activeNetwork.chainId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(estimate -> confirmationDialog.setGasEstimate(estimate), @@ -1446,6 +1278,23 @@ private void txError(Throwable throwable) confirmationDialog.dismiss(); } + private void displayError(int title, int text) + { + if (resultDialog != null && resultDialog.isShowing()) resultDialog.dismiss(); + resultDialog = new AWalletAlertDialog(requireContext()); + resultDialog.setIcon(ERROR); + resultDialog.setTitle(title); + if (text != 0) resultDialog.setMessage(text); + resultDialog.setButtonText(R.string.button_ok); + resultDialog.setButtonListener(v -> { + resultDialog.dismiss(); + }); + resultDialog.show(); + + if (confirmationDialog != null && confirmationDialog.isShowing()) + confirmationDialog.dismiss(); + } + private void showWalletWatch() { if (resultDialog != null && resultDialog.isShowing()) resultDialog.dismiss(); @@ -1487,96 +1336,36 @@ else if (transaction.payload == null && transaction.value == null) resultDialog.show(); } + @Override public void backPressed() { - if (web3 == null || back == null || back.getAlpha() == 0.3f) return; if (!currentFragment.equals(DAPP_BROWSER)) { detachFragment(currentFragment); - checkBackClickArrowVisibility(); } else if (web3.canGoBack()) { - checkBackClickArrowVisibility(); //to make arrows function correctly - don't want to wait for web page to load to check back/forwards - this looks clunky - loadSessionUrl(-1); + setUrlText(getSessionUrl(-1)); web3.goBack(); detachFragments(); } else if (!web3.getUrl().equalsIgnoreCase(getDefaultDappUrl())) { - //load homepage - homePressed = true; - web3.resetView(); - web3.loadUrl(getDefaultDappUrl()); - setUrlText(getDefaultDappUrl()); - checkBackClickArrowVisibility(); - } - else - { - checkBackClickArrowVisibility(); + homePressed(); + addressBar.updateNavigationButtons(web3.copyBackForwardList()); } } private void goToNextPage() { - if (next.getAlpha() == 0.3f) return; if (web3.canGoForward()) { - checkForwardClickArrowVisibility(); - loadSessionUrl(1); + setUrlText(getSessionUrl(1)); web3.goForward(); } } - /** - * Check if this is the last web item and the last fragment item. - */ - private void checkBackClickArrowVisibility() - { - //will this be last item? - WebBackForwardList sessionHistory = web3.copyBackForwardList(); - int nextIndex = sessionHistory.getCurrentIndex() - 1; - - String nextUrl; - - if (nextIndex >= 0) - { - WebHistoryItem newItem = sessionHistory.getItemAtIndex(nextIndex); - nextUrl = newItem.getUrl(); - } - else - { - nextUrl = urlTv.getText().toString();// web3.getUrl();// getDefaultDappUrl(); - } - - if (nextUrl.equalsIgnoreCase(getDefaultDappUrl())) - { - back.setAlpha(0.3f); - } - else - { - back.setAlpha(1.0f); - } - } - - /** - * After a forward click while web browser active, check if forward and back arrows should be updated. - * Note that the web item only becomes history after the next page is loaded, so if the next item is new, then - */ - private void checkForwardClickArrowVisibility() - { - WebBackForwardList sessionHistory = web3.copyBackForwardList(); - int nextIndex = sessionHistory.getCurrentIndex() + 1; - if (nextIndex >= sessionHistory.getSize() - 1) next.setAlpha(0.3f); - else next.setAlpha(1.0f); - } - - /** - * Browse to relative entry with sanity check on value - * - * @param relative relative addition or subtraction of browsing index - */ - private void loadSessionUrl(int relative) + private String getSessionUrl(int relative) { WebBackForwardList sessionHistory = web3.copyBackForwardList(); int newIndex = sessionHistory.getCurrentIndex() + relative; @@ -1585,9 +1374,11 @@ private void loadSessionUrl(int relative) WebHistoryItem newItem = sessionHistory.getItemAtIndex(newIndex); if (newItem != null) { - setUrlText(newItem.getUrl()); + return newItem.getUrl(); } } + + return ""; } @Override @@ -1607,19 +1398,19 @@ public void onWebpageLoaded(String url, String title) { DApp dapp = new DApp(title, url); DappBrowserUtils.addToHistory(getContext(), dapp); - adapter.addSuggestion(dapp); + addressBar.addSuggestion(dapp); } onWebpageLoadComplete(); - if (urlTv != null) urlTv.setText(url); + addressBar.setUrl(url); } @Override public void onWebpageLoadComplete() { handler.post(() -> { - setBackForwardButtons(); + addressBar.updateNavigationButtons(web3.copyBackForwardList()); if (loadUrlAfterReload != null) { loadUrl(loadUrlAfterReload); @@ -1633,63 +1424,12 @@ public void onWebpageLoadComplete() } } - private void setBackForwardButtons() - { - WebBackForwardList sessionHistory; - boolean canBrowseBack = false; - boolean canBrowseForward = false; - - if (currentFragment != null && !currentFragment.equals(DAPP_BROWSER)) - { - canBrowseBack = true; - } - else if (web3 != null) - { - sessionHistory = web3.copyBackForwardList(); - canBrowseBack = !isOnHomePage(); - canBrowseForward = (sessionHistory != null && sessionHistory.getCurrentIndex() < sessionHistory.getSize() - 1); - } - - if (back != null) - { - if (canBrowseBack) - { - back.setAlpha(1.0f); - } - else - { - back.setAlpha(0.3f); - } - } - - if (next != null) - { - if (canBrowseForward) - { - next.setAlpha(1.0f); - } - else - { - next.setAlpha(0.3f); - } - } - } - - private boolean isOnHomePage() - { - if (web3 != null) - { - String url = web3.getUrl(); - return EthereumNetworkRepository.isDefaultDapp(url); - } - else - { - return false; - } - } - private boolean loadUrl(String urlText) { + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_URL, urlText); + viewModel.track(Analytics.Action.LOAD_URL, props); + detachFragments(); addToBackStack(DAPP_BROWSER); cancelSearchSession(); @@ -1706,8 +1446,7 @@ public void loadDirect(String urlText) { if (web3 == null) { - if (getActivity() != null) - ((HomeActivity) getActivity()).resetFragment(WalletPage.DAPP_BROWSER); + requireActivity().recreate(); loadOnInit = urlText; } else @@ -1719,9 +1458,12 @@ public void loadDirect(String urlText) setUrlText(Utils.formatUrl(urlText)); web3.resetView(); web3.loadUrl(Utils.formatUrl(urlText)); - //ensure focus isn't on the keyboard - KeyboardUtils.hideKeyboard(urlTv); + addressBar.leaveEditMode(); web3.requestFocus(); + + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_URL, urlText); + viewModel.track(Analytics.Action.LOAD_URL, props); } } @@ -1735,34 +1477,8 @@ public void reloadPage() } web3.resetView(); web3.reload(); - } - } - - @Override - public void onItemClick(String url) - { - addToBackStack(DAPP_BROWSER); - loadUrl(url); - } - - public void testRecoverAddressFromSignature(@NotNull String message, String sig) - { - String prefix = PERSONAL_MESSAGE_PREFIX + message.length(); - byte[] msgHash = (prefix + message).getBytes(); - byte[] signatureBytes = Numeric.hexStringToByteArray(sig); - Sign.SignatureData sd = sigFromByteArray(signatureBytes); - String addressRecovered; - - try - { - BigInteger recoveredKey = Sign.signedMessageToKey(msgHash, sd); - addressRecovered = "0x" + Keys.getAddress(recoveredKey); - Timber.d("Recovered: %s", addressRecovered); - } - catch (SignatureException e) - { - e.printStackTrace(); + viewModel.track(Analytics.Action.RELOAD_BROWSER); } } @@ -1775,6 +1491,7 @@ private void resetDappBrowser() setUrlText(getDefaultDappUrl()); } + @Override public void handleQRCode(int resultCode, Intent data, FragmentMessenger messenger) { //result @@ -1816,10 +1533,10 @@ public void handleQRCode(int resultCode, Intent data, FragmentMessenger messenge } } break; - case QRScanner.DENY_PERMISSION: + case QRScannerActivity.DENY_PERMISSION: showCameraDenied(); break; - case QRScanner.WALLET_CONNECT: + case QRScannerActivity.WALLET_CONNECT: return; default: break; @@ -1945,6 +1662,7 @@ private void requestCameraPermission(@NotNull PermissionRequest request) } } + @Override public void gotCameraAccess(@NotNull String[] permissions, int[] grantResults) { boolean cameraAccess = false; @@ -1961,6 +1679,7 @@ public void gotCameraAccess(@NotNull String[] permissions, int[] grantResults) Toast.makeText(getContext(), "Permission not given", Toast.LENGTH_SHORT).show(); } + @Override public void gotGeoAccess(@NotNull String[] permissions, int[] grantResults) { boolean geoAccess = false; @@ -1978,6 +1697,7 @@ public void gotGeoAccess(@NotNull String[] permissions, int[] grantResults) geoCallback.invoke(geoOrigin, geoAccess, false); } + @Override public void gotFileAccess(@NotNull String[] permissions, int[] grantResults) { boolean fileAccess = false; @@ -2042,9 +1762,7 @@ private ByteArrayOutputStream getSerialisedBundle(Bundle bundle) throws Exceptio oos.writeObject(CURRENT_FRAGMENT); oos.writeObject(currentFragment); oos.writeObject(CURRENT_URL); - String uurl = urlTv.getText().toString(); - String uurl2 = web3.getUrl(); - oos.writeObject(urlTv.getText().toString()); + oos.writeObject(addressBar.getUrl()); } return bos; } @@ -2110,33 +1828,6 @@ public void gotAuthorisation(boolean gotAuth) } } - @Override - public void gotAuthorisationForSigning(boolean gotAuth, Signable messageToSign) - { - if (gotAuth) - { - viewModel.completeAuthentication(SIGN_DATA); - viewModel.signMessage(messageToSign, dAppFunction); - } - else - { - web3.onSignCancel(messageToSign.getCallbackId()); - } - } - - /** - * Endpoint from PIN/Swipe authorisation - * - * @param gotAuth - */ - public void pinAuthorisation(boolean gotAuth) - { - if (confirmationDialog != null && confirmationDialog.isShowing()) - { - confirmationDialog.completeSignRequest(gotAuth); - } - } - @Override public void buttonClick(long callbackId, Token baseToken) { @@ -2170,7 +1861,8 @@ public void getAuthorisation(SignAuthenticationCallback callback) @Override public void sendTransaction(Web3Transaction finalTx) { - final SendTransactionInterface callback = new SendTransactionInterface() { + final SendTransactionInterface callback = new SendTransactionInterface() + { @Override public void transactionSuccess(Web3Transaction web3Tx, String hashData) { @@ -2204,7 +1896,10 @@ public void dismissed(String txHash, long callbackId, boolean actionCompleted) @Override public void notifyConfirm(String mode) { - if (getActivity() != null) ((HomeActivity) getActivity()).useActionSheet(mode); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + props.put(Analytics.PROPS_ACTION_SHEET_SOURCE, ActionSheetSource.BROWSER); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, props); } @Override @@ -2235,16 +1930,25 @@ private String determineMimeType(@NotNull WebChromeClient.FileChooserParams file case "svg": case "jpg": case "jpeg": - case "image/*": - mime = "image/*"; + case "bmp": + mime = "image/" + firstType; break; case "mp4": case "x-msvideo": case "x-ms-wmv": case "mpeg4-generic": + case "webm": + case "avi": + case "mpg": + case "m2v": + mime = "video/" + firstType; + break; + + case "image/*": + case "audio/*": case "video/*": - mime = "video/*"; + mime = firstType; break; case "mpeg": @@ -2253,8 +1957,7 @@ private String determineMimeType(@NotNull WebChromeClient.FileChooserParams file case "ogg": case "midi": case "x-ms-wma": - case "audio/*": - mime = "audio/*"; + mime = "audio/" + firstType; break; case "pdf": @@ -2263,7 +1966,7 @@ private String determineMimeType(@NotNull WebChromeClient.FileChooserParams file case "xml": case "csv": - mime = "text/*"; + mime = "text/" + firstType; break; default: @@ -2277,7 +1980,7 @@ private String determineMimeType(@NotNull WebChromeClient.FileChooserParams file private String getDefaultDappUrl() { String customHome = viewModel.getHomePage(getContext()); - return customHome != null ? customHome : EthereumNetworkRepository.defaultDapp(activeNetwork != null ? activeNetwork.chainId : 0); + return customHome != null ? customHome : DappBrowserUtils.defaultDapp(activeNetwork != null ? activeNetwork.chainId : 0); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java index 8f50bea808..c5cc6ed960 100644 --- a/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java @@ -28,6 +28,7 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.AddressMode; import com.alphawallet.app.entity.BuyCryptoInterface; import com.alphawallet.app.entity.StandardFunctionInterface; @@ -510,6 +511,7 @@ public void handleBuyFunction(Token token) { Intent intent = viewModel.getBuyIntent(wallet.address, token); setResult(RESULT_OK, intent); + viewModel.track(Analytics.Action.BUY_WITH_RAMP); finish(); } diff --git a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java index dc81d9688b..bedb6faf01 100644 --- a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java @@ -18,7 +18,6 @@ import android.view.View; import android.webkit.WebView; import android.widget.LinearLayout; -import android.widget.ProgressBar; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -29,10 +28,10 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.DApp; -import com.alphawallet.app.entity.DAppFunction; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TransactionData; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokenscript.TokenScriptRenderCallback; import com.alphawallet.app.entity.tokenscript.WebCompletionCallback; @@ -49,7 +48,9 @@ import com.alphawallet.app.web3.entity.PageReadyCallback; import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.widget.ActionSheet; import com.alphawallet.app.widget.ActionSheetDialog; +import com.alphawallet.app.widget.ActionSheetSignDialog; import com.alphawallet.app.widget.FunctionButtonBar; import com.alphawallet.app.widget.SignTransactionDialog; import com.alphawallet.ethereum.EthereumNetworkBase; @@ -103,7 +104,7 @@ public class FunctionActivity extends BaseActivity implements FunctionCallback, private int parsePass = 0; private int resolveInputCheckCount; private TSAction action; - private ActionSheetDialog confirmationDialog; + private ActionSheet confirmationDialog; private void initViews() { actionMethod = getIntent().getStringExtra(C.EXTRA_STATE); @@ -127,7 +128,7 @@ private void initViews() { tokenView.setChainId(token.tokenInfo.chainId); tokenView.setWalletAddress(new Address(token.getWallet())); tokenView.setupWindowCallback(this); - tokenView.setRpcUrl(token.tokenInfo.chainId); + tokenView.setRpcUrl(viewModel.getBrowserRPC(token.tokenInfo.chainId)); tokenView.setOnReadyCallback(this); tokenView.setOnSignPersonalMessageListener(this); tokenView.setOnSetValuesListener(this); @@ -135,9 +136,6 @@ private void initViews() { viewModel.startGasPriceUpdate(token.tokenInfo.chainId); viewModel.getCurrentWallet(); parsePass = 0; - - ProgressBar loadSpinner = findViewById(R.id.ticket_load_spinner); - handler.postDelayed(() -> loadSpinner.setVisibility(View.GONE), 2500); } private void displayFunction(String tokenAttrs) @@ -176,6 +174,12 @@ private void getAttrs() // Fetch attributes local to this action and add them to the injected token properties Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + if (functions == null) + { + recreate(); + return; + } + action = functions.get(actionMethod); List localAttrs = (action != null && action.attributes != null) ? new ArrayList<>(action.attributes.values()) : null; @@ -231,7 +235,7 @@ private void onAttr(TokenScriptResult.Attribute attribute) { //is the attr incomplete? Timber.d("ATTR/FA: " + attribute.id + " (" + attribute.name + ")" + " : " + attribute.text); - TokenScriptResult.addPair(attrs, attribute.id, attribute.text); + TokenScriptResult.addPair(attrs, attribute); } private void fillEmpty() @@ -304,6 +308,7 @@ public boolean onOptionsItemSelected(MenuItem item) { private void completeTokenScriptFunction(String function) { Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + if (functions == null) return; action = functions.get(function); if (action != null && action.function != null) //if no function then it's handled by the token view @@ -548,12 +553,6 @@ private void showTransactionError() alertDialog.show(); } - @Override - public void signMessage(Signable message, DAppFunction dAppFunction) - { - viewModel.signMessage(message, dAppFunction, token.tokenInfo.chainId); - } - @Override public void functionSuccess() { @@ -637,8 +636,7 @@ public void onRestoreInstanceState(Bundle savedInstanceState) public void onSignPersonalMessage(EthereumMessage message) { //pop open the actionsheet - confirmationDialog = new ActionSheetDialog(this, this, this, message); - confirmationDialog.setCanceledOnTouchOutside(false); + confirmationDialog = new ActionSheetSignDialog(this, this, message); //new ActionSheetDialog(this, this, this, message); confirmationDialog.show(); confirmationDialog.fullExpand(); } @@ -784,39 +782,19 @@ public void gotAuthorisation(boolean gotAuth) } @Override - public void gotAuthorisationForSigning(boolean gotAuth, Signable messageToSign) + public void signingComplete(SignatureFromKey signature, Signable message) { - viewModel.completeAuthentication(SIGN_DATA); - - DAppFunction dAppFunction = new DAppFunction() - { - @Override - public void DAppError(Throwable error, Signable message) - { - confirmationDialog.dismiss(); - tokenView.onSignCancel(message); - functionFailed(); - } - - @Override - public void DAppReturn(byte[] data, Signable message) - { - String signHex = Numeric.toHexString(data); - signHex = Numeric.cleanHexPrefix(signHex); - tokenView.onSignPersonalMessageSuccessful(message, signHex); - testRecoverAddressFromSignature(message.getMessage(), signHex); - confirmationDialog.success(); - } - }; + String signHex = Numeric.toHexString(signature.signature); + signHex = Numeric.cleanHexPrefix(signHex); + tokenView.onSignPersonalMessageSuccessful(message, signHex); + testRecoverAddressFromSignature(message.getMessage(), signHex); + } - if (gotAuth) - { - signMessage(messageToSign, dAppFunction); - } - else - { - confirmationDialog.dismiss(); - } + @Override + public void signingFailed(Throwable error, Signable message) + { + tokenView.onSignCancel(message); + functionFailed(); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/GasSettingsActivity.java b/app/src/main/java/com/alphawallet/app/ui/GasSettingsActivity.java index 6c2d9f1df4..6b1374f927 100644 --- a/app/src/main/java/com/alphawallet/app/ui/GasSettingsActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/GasSettingsActivity.java @@ -44,6 +44,7 @@ import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.GasSettingsViewModel; import com.alphawallet.app.widget.GasSliderView; +import com.alphawallet.token.tools.Convert; import com.google.android.material.radiobutton.MaterialRadioButton; import java.math.BigDecimal; @@ -59,7 +60,7 @@ @AndroidEntryPoint public class GasSettingsActivity extends BaseActivity implements GasSettingsCallback { - private static final int GAS_PRECISION = 5; //5 dp for gas + public static final int GAS_PRECISION = 5; //5 dp for gas GasSettingsViewModel viewModel; @@ -376,7 +377,15 @@ else if (position != TXSpeed.CUSTOM && currentGasSpeedIndex == TXSpeed.CUSTOM) BigDecimal maxGas = BalanceUtils.weiToGweiBI(gs.gasPrice.maxFeePerGas); String speedGwei; - if (maxGas.compareTo(BigDecimal.valueOf(2)) < 0) + BigDecimal ethAmount = Convert.fromWei(new BigDecimal(gs.gasPrice.maxFeePerGas), Convert.Unit.ETHER); + + if (BalanceUtils.requiresSmallGweiValueSuffix(ethAmount)) + { + speedGwei = context.getString(R.string.token_balance, + BalanceUtils.getSlidingBaseValue(new BigDecimal(gs.gasPrice.maxFeePerGas), 18, GAS_PRECISION), + baseCurrency.getSymbol()); + } + else if (maxGas.compareTo(BigDecimal.valueOf(2)) < 0) { speedGwei = BalanceUtils.weiToGwei(new BigDecimal(gs.gasPrice.maxFeePerGas), 2); } @@ -426,7 +435,7 @@ else if (position != TXSpeed.CUSTOM && currentGasSpeedIndex == TXSpeed.CUSTOM) BigDecimal gasFee = new BigDecimal(gs.gasPrice.maxFeePerGas).multiply(useGasLimit); - String gasAmountInBase = BalanceUtils.getScaledValueScientific(gasFee, baseCurrency.tokenInfo.decimals, GAS_PRECISION); + String gasAmountInBase = BalanceUtils.getSlidingBaseValue(gasFee, baseCurrency.tokenInfo.decimals, GAS_PRECISION); if (gasAmountInBase.equals("0")) gasAmountInBase = "0.00001"; //NB no need to allow for zero gas chains; this activity wouldn't appear String displayStr = context.getString(R.string.gas_amount, gasAmountInBase, baseCurrency.getSymbol()); diff --git a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java index 63e5dc9306..b379b3e0d7 100644 --- a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java @@ -33,11 +33,12 @@ import android.widget.LinearLayout; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; @@ -50,9 +51,9 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.api.v1.entity.request.ApiV1Request; import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.CryptoFunctions; @@ -61,74 +62,76 @@ import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.HomeCommsInterface; import com.alphawallet.app.entity.HomeReceiver; -import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletPage; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.router.ImportTokenRouter; import com.alphawallet.app.service.NotificationService; import com.alphawallet.app.service.PriceAlertsService; +import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; import com.alphawallet.app.ui.widget.entity.PagerCallback; import com.alphawallet.app.util.LocaleUtils; import com.alphawallet.app.util.UpdateUtils; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.BaseNavigationActivity; import com.alphawallet.app.viewmodel.HomeViewModel; +import com.alphawallet.app.viewmodel.WalletConnectViewModel; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.alphawallet.app.walletconnect.WCSession; +import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.AWalletConfirmationDialog; -import com.alphawallet.app.widget.SignTransactionDialog; import com.alphawallet.token.entity.SalesOrderMalformed; +import com.alphawallet.token.entity.Signable; +import com.alphawallet.token.tools.Numeric; import com.alphawallet.token.tools.ParseMagicLink; import com.github.florent37.tutoshowcase.TutoShowcase; import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent; -import java.io.File; import java.lang.reflect.Method; import java.net.URLDecoder; import java.util.List; +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; import timber.log.Timber; @AndroidEntryPoint public class HomeActivity extends BaseNavigationActivity implements View.OnClickListener, HomeCommsInterface, - FragmentMessenger, Runnable, SignAuthenticationCallback, LifecycleObserver, PagerCallback + FragmentMessenger, Runnable, SignAuthenticationCallback, ActionSheetCallback, LifecycleObserver, PagerCallback { - private HomeViewModel viewModel; + @Inject + AWWalletConnectClient awWalletConnectClient; + + public static final int RC_ASSET_EXTERNAL_WRITE_PERM = 223; + public static final int RC_ASSET_NOTIFICATION_PERM = 224; + public static final int DAPP_BARCODE_READER_REQUEST_CODE = 1; + public static final String STORED_PAGE = "currentPage"; + public static final String RESET_TOKEN_SERVICE = "HOME_reset_ts"; + public static final String AW_MAGICLINK = "aw.app/"; + public static final String AW_MAGICLINK_DIRECT = "openurl?url="; + private static boolean updatePrompt = false; + private final FragmentStateAdapter pager2Adapter; + private final Handler handler = new Handler(Looper.getMainLooper()); + private final ActivityResultLauncher networkSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> getSupportFragmentManager().setFragmentResult(RESET_TOKEN_SERVICE, new Bundle())); + private HomeViewModel viewModel; + private WalletConnectViewModel viewModelWC; private Dialog dialog; private ViewPager2 viewPager; - private final FragmentStateAdapter pager2Adapter; private LinearLayout successOverlay; private ImageView successImage; - private final Handler handler = new Handler(Looper.getMainLooper()); private HomeReceiver homeReceiver; - private String buildVersion; - private Fragment settingsFragment; - private Fragment dappBrowserFragment; - private Fragment walletFragment; - private Fragment activityFragment; private String walletTitle; - private static boolean updatePrompt = false; private TutoShowcase backupWalletDialog; private boolean isForeground; private volatile boolean tokenClicked = false; private String openLink; - private boolean inWalletConnect; - - public static final int RC_DOWNLOAD_EXTERNAL_WRITE_PERM = 222; - public static final int RC_ASSET_EXTERNAL_WRITE_PERM = 223; - public static final int RC_ASSET_NOTIFICATION_PERM = 224; - - public static final int DAPP_BARCODE_READER_REQUEST_CODE = 1; - public static final int DAPP_TRANSACTION_SEND_REQUEST = 2; - public static final String STORED_PAGE = "currentPage"; - public static final String RESET_TOKEN_SERVICE = "HOME_reset_ts"; - public static final String AW_MAGICLINK = "aw.app/"; - public static final String AW_MAGICLINK_DIRECT = "openurl?url="; public HomeActivity() { @@ -136,6 +139,13 @@ public HomeActivity() pager2Adapter = new ScreenSlidePagerAdapter(this); } + public static void setUpdatePrompt() + { + //TODO: periodically check this value (eg during page flipping) + //Set alert to user to update their app + updatePrompt = true; + } + @OnLifecycleEvent(Lifecycle.Event.ON_START) private void onMoveToForeground() { @@ -198,12 +208,16 @@ protected void onCreate(@Nullable Bundle savedInstanceState) LocaleUtils.setActiveLocale(this); getLifecycle().addObserver(this); isForeground = true; + setWCConnect(); if (getSupportActionBar() != null) getSupportActionBar().hide(); viewModel = new ViewModelProvider(this) .get(HomeViewModel.class); - viewModel.identify(this); + viewModelWC = new ViewModelProvider(this) + .get(WalletConnectViewModel.class); + + viewModel.identify(); viewModel.setWalletStartup(); viewModel.setCurrencyAndLocale(this); viewModel.tryToShowWhatsNewDialog(this); @@ -241,10 +255,11 @@ public void onPageScrollStateChanged(int state) dissableDisplayHomeAsUp(); viewModel.error().observe(this, this::onError); - viewModel.installIntent().observe(this, this::onInstallIntent); viewModel.walletName().observe(this, this::onWalletName); viewModel.backUpMessage().observe(this, this::onBackup); viewModel.splashReset().observe(this, this::onRequireInit); + viewModel.defaultWallet().observe(this, this::onDefaultWallet); + viewModel.updateAvailable().observe(this, this::onUpdateAvailable); if (CustomViewSettings.hideDappBrowser()) { @@ -275,8 +290,10 @@ public void onPageScrollStateChanged(int state) } else { - //TODO: Check we are using latest version on github, since we're using a downloaded/manually installed version - //First check that this the package name is "io.stormbird.wallet" - it could be a fork + if (getApplicationContext().getPackageName().equals("io.stormbird.wallet")) + { + viewModel.checkLatestGithubRelease(); + } } setupFragmentListeners(); @@ -305,6 +322,34 @@ public void onPageScrollStateChanged(int state) startService(i); } + private void onUpdateAvailable(String availableVersion) + { + externalUpdateReady(availableVersion); + } + + private void setWCConnect() + { + try + { + awWalletConnectClient.init(this); + } + catch (Exception e) + { + Timber.tag("WalletConnect").e(e); + } + } + + private void onDefaultWallet(Wallet wallet) + { + if (viewModel.checkNewWallet(wallet.address)) + { + viewModel.setNewWallet(wallet.address, false); + Intent selectNetworkIntent = new Intent(this, SelectNetworkFilterActivity.class); + selectNetworkIntent.putExtra(C.EXTRA_SINGLE_ITEM, false); + networkSettingsHandler.launch(selectNetworkIntent); + } + } + private void setupFragmentListeners() { //TODO: Move all fragment comms to this model - see all instances of ((HomeActivity)getActivity()). @@ -340,7 +385,7 @@ private void setupFragmentListeners() List contractList = b.getParcelableArrayList(ADDED_TOKEN); if (contractList != null) { - ((ActivityFragment) getFragment(ACTIVITY)).addedToken(contractList); + getFragment(ACTIVITY).addedToken(contractList); } }); @@ -476,15 +521,7 @@ private void onWalletName(String name) walletTitle = getString(R.string.toolbar_header_wallet); } - // putting in a try catch to avoid crashing the app - try - { - getFragment(WALLET).setToolbarTitle(walletTitle); - } - catch (Exception e) - { - Timber.e(e); - } + getFragment(WALLET).setToolbarTitle(walletTitle); } private void onError(ErrorEnvelope errorEnvelope) @@ -497,6 +534,7 @@ private void onError(ErrorEnvelope errorEnvelope) protected void onResume() { super.onResume(); + setWCConnect(); viewModel.prepare(this); viewModel.getWalletName(this); viewModel.setErrorCallback(this); @@ -588,7 +626,7 @@ public boolean onBottomNavigationItemSelected(WalletPage index) public void onBrowserWithURL(String url) { showPage(DAPP_BROWSER); - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).onItemClick(url); + getFragment(DAPP_BROWSER).onItemClick(url); } @Override @@ -704,11 +742,17 @@ else if (warns > 10) } @Override - public void updateReady(int updateVersion) + public void playStoreUpdateReady(int updateVersion) { //signal to WalletFragment an update is ready //display entry in the WalletView - ((NewSettingsFragment) getFragment(SETTINGS)).signalUpdate(updateVersion); + getFragment(SETTINGS).signalPlayStoreUpdate(updateVersion); + } + + @Override + public void externalUpdateReady(String updateVersion) + { + getFragment(SETTINGS).signalExternalUpdate(updateVersion); } @Override @@ -736,18 +780,18 @@ public void tokenScriptError(String message) void backupWalletFail(String keyBackup, boolean hasNoLock) { //postpone backup until later - ((NewSettingsFragment) getFragment(SETTINGS)).backupSeedSuccess(hasNoLock); + getFragment(SETTINGS).backupSeedSuccess(hasNoLock); if (keyBackup != null) { - ((WalletFragment) getFragment(WALLET)).remindMeLater(new Wallet(keyBackup)); + getFragment(WALLET).remindMeLater(new Wallet(keyBackup)); viewModel.checkIsBackedUp(keyBackup); } } void backupWalletSuccess(String keyBackup) { - ((NewSettingsFragment) getFragment(SETTINGS)).backupSeedSuccess(false); - ((WalletFragment) getFragment(WALLET)).storeWalletBackupTime(keyBackup); + getFragment(SETTINGS).backupSeedSuccess(false); + getFragment(WALLET).storeWalletBackupTime(keyBackup); removeSettingsBadgeKey(C.KEY_NEEDS_BACKUP); if (successImage != null) successImage.setImageResource(R.drawable.big_green_tick); if (successOverlay != null) successOverlay.setVisibility(View.VISIBLE); @@ -788,24 +832,6 @@ public void createdKey(String keyAddress) //viewModel.upgradeWallet(keyAddress); } - /** - * On restarting the wallet, all fragments check they have their viewModels - * If they do not, then the onResume override will call this resetFragment method for that fragment - * Which rebuilds the view and repopulates all the view members required for operation - * - * @param fragmentId - */ - public void resetFragment(WalletPage fragmentId) - { - Fragment fragment = getFragment(fragmentId); - - getSupportFragmentManager() - .beginTransaction() - .detach(fragment) - .attach(fragment) - .commitAllowingStateLoss(); - } - @Override public void loadingComplete() { @@ -834,103 +860,18 @@ else if (lastId >= 0 && lastId < WalletPage.values().length) } } - private class ScreenSlidePagerAdapter extends FragmentStateAdapter - { - public ScreenSlidePagerAdapter(@NonNull FragmentActivity fragmentActivity) - { - super(fragmentActivity); - } - - @NonNull - @Override - public Fragment createFragment(int position) - { - switch (WalletPage.values()[position]) - { - case WALLET: - default: - walletFragment = new WalletFragment(); - return walletFragment; - case ACTIVITY: - activityFragment = new ActivityFragment(); - return activityFragment; - case DAPP_BROWSER: - if (CustomViewSettings.hideDappBrowser()) dappBrowserFragment = new Fragment(); - else dappBrowserFragment = new DappBrowserFragment(); - return dappBrowserFragment; - case SETTINGS: - settingsFragment = new NewSettingsFragment(); - return settingsFragment; - } - } - - @Override - public int getItemCount() - { - return WalletPage.values().length; - } - - } - private BaseFragment getFragment(WalletPage page) { - //build map, return correct fragment. - if (getSupportFragmentManager().getFragments().size() < page.ordinal()) + // if fragment hasn't been created yet, return a blank BaseFragment to avoid crash + if ((page.ordinal() + 1) > getSupportFragmentManager().getFragments().size()) { - switch (page) - { - default: - case WALLET: - return (BaseFragment) walletFragment; - case ACTIVITY: - return (BaseFragment) activityFragment; - case DAPP_BROWSER: - return (BaseFragment) dappBrowserFragment; - case SETTINGS: - return (BaseFragment) settingsFragment; - } - } - else return (BaseFragment) getSupportFragmentManager().getFragments().get(page.ordinal()); - } - - @Override - public void downloadReady(String build) - { - hideDialog(); - buildVersion = build; - //display download ready popup - //Possibly only show this once per day otherwise too annoying! - int asks = viewModel.getUpdateAsks() + 1; - AWalletConfirmationDialog dialog = new AWalletConfirmationDialog(this); - dialog.setTitle(R.string.new_version_title); - dialog.setSmallText(R.string.new_version); - String newBuild = "New version: " + build; - dialog.setMediumText(newBuild); - dialog.setPrimaryButtonText(R.string.confirm_update); - dialog.setPrimaryButtonListener(v -> - { - if (checkWritePermission(RC_DOWNLOAD_EXTERNAL_WRITE_PERM)) - { - viewModel.downloadAndInstall(build, this); - } - dialog.dismiss(); - }); - if (asks > 1) - { - dialog.setSecondaryButtonText(R.string.dialog_not_again); + recreate(); //restart activity required + return new BaseFragment(); } else { - dialog.setSecondaryButtonText(R.string.dialog_later); + return (BaseFragment) getSupportFragmentManager().getFragments().get(page.ordinal()); } - dialog.setSecondaryButtonListener(v -> - { - //only dismiss twice before we stop warning. - viewModel.setUpdateAsksCount(asks); - dialog.dismiss(); - }); - this.dialog = dialog; - dialog.show(); } @Override @@ -948,14 +889,14 @@ public void backupSuccess(String keyAddress) @Override public void resetTokens() { - ((ActivityFragment) getFragment(ACTIVITY)).resetTokens(); - ((WalletFragment) getFragment(WALLET)).resetTokens(); + getFragment(ACTIVITY).resetTokens(); + getFragment(WALLET).resetTokens(); } @Override public void resetTransactions() { - ((ActivityFragment) getFragment(ACTIVITY)).resetTransactions(); + getFragment(ACTIVITY).resetTransactions(); } @Override @@ -978,30 +919,6 @@ private void hideDialog() } } - private boolean checkWritePermission(int permissionTag) - { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) - { - return true; - } - else - { - final String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE)) - { - Timber.tag("HomeActivity").w("Folder write permission is not granted. Requesting permission"); - ActivityCompat.requestPermissions(this, permissions, permissionTag); - return false; - } - else - { - return true; - } - } - } - private boolean checkNotificationPermission(int permissionTag) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NOTIFICATION_POLICY) @@ -1033,23 +950,13 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in switch (requestCode) { case DappBrowserFragment.REQUEST_CAMERA_ACCESS: - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).gotCameraAccess(permissions, grantResults); + getFragment(DAPP_BROWSER).gotCameraAccess(permissions, grantResults); break; case DappBrowserFragment.REQUEST_FILE_ACCESS: - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).gotFileAccess(permissions, grantResults); + getFragment(DAPP_BROWSER).gotFileAccess(permissions, grantResults); break; case DappBrowserFragment.REQUEST_FINE_LOCATION: - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).gotGeoAccess(permissions, grantResults); - break; - case RC_DOWNLOAD_EXTERNAL_WRITE_PERM: - if (hasPermission(permissions, grantResults)) - { - viewModel.downloadAndInstall(buildVersion, this); - } - else - { - showRequirePermissionError(); - } + getFragment(DAPP_BROWSER).gotGeoAccess(permissions, grantResults); break; case RC_ASSET_EXTERNAL_WRITE_PERM: //Can't get here @@ -1057,74 +964,15 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in } } - private boolean hasPermission(String[] permissions, int[] grantResults) - { - boolean hasPermission = true; - for (int i = 0; i < permissions.length; i++) - { - if (grantResults[i] == -1) - { - hasPermission = false; - break; - } - } - - return hasPermission; - } - - private void showRequirePermissionError() - { - AWalletAlertDialog aDialog = new AWalletAlertDialog(this); - aDialog.setIcon(AWalletAlertDialog.ERROR); - aDialog.setTitle(R.string.install_error); - aDialog.setMessage(R.string.require_write_permission); - aDialog.setButtonText(R.string.action_cancel); - aDialog.setButtonListener(v -> - { - aDialog.dismiss(); - }); - aDialog.show(); - } - - private void onInstallIntent(File installFile) - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - String authority = BuildConfig.APPLICATION_ID + ".fileprovider"; - Uri apkUri = FileProvider.getUriForFile(getApplicationContext(), authority, installFile); - Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); - intent.setData(apkUri); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(intent); - } - else - { - Uri apkUri = Uri.fromFile(installFile); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } - - //Blank install time here so that next time the app runs the install time will be correctly set up - viewModel.setInstallTime(0); - finish(); - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - Operation taskCode = null; - if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) - { - taskCode = Operation.values()[requestCode - SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS]; - requestCode = SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS; - } + super.onActivityResult(requestCode, resultCode, data); // intercept return intent from PIN/Swipe authentications switch (requestCode) { case DAPP_BARCODE_READER_REQUEST_CODE: - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).handleQRCode(resultCode, data, this); + getFragment(DAPP_BROWSER).handleQRCode(resultCode, data, this); break; case C.REQUEST_BACKUP_WALLET: String keyBackup = null; @@ -1134,16 +982,6 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) if (resultCode == RESULT_OK) backupWalletSuccess(keyBackup); else backupWalletFail(keyBackup, noLockScreen); break; - case SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS: - switch (getSelectedItem()) - { - case DAPP_BROWSER: - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).pinAuthorisation(resultCode == RESULT_OK); - break; - default: - break; - } - break; case C.REQUEST_UNIVERSAL_SCAN: if (data != null && resultCode == Activity.RESULT_OK) { @@ -1166,7 +1004,7 @@ else if (data.hasExtra(C.EXTRA_ACTION_NAME)) case C.TOKEN_SEND_ACTIVITY: if (data != null && resultCode == Activity.RESULT_OK && data.hasExtra(C.DAPP_URL_LOAD)) { - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).switchNetworkAndLoadUrl(data.getLongExtra(C.EXTRA_CHAIN_ID, MAINNET_ID), + getFragment(DAPP_BROWSER).switchNetworkAndLoadUrl(data.getLongExtra(C.EXTRA_CHAIN_ID, MAINNET_ID), data.getStringExtra(C.DAPP_URL_LOAD)); showPage(DAPP_BROWSER); } @@ -1178,7 +1016,7 @@ else if (data != null && resultCode == Activity.RESULT_OK && data.hasExtra(C.EXT case C.TERMINATE_ACTIVITY: if (data != null && resultCode == Activity.RESULT_OK) { - ((ActivityFragment) getFragment(ACTIVITY)).scrollToTop(); + getFragment(ACTIVITY).scrollToTop(); showPage(ACTIVITY); } break; @@ -1186,7 +1024,13 @@ else if (data != null && resultCode == Activity.RESULT_OK && data.hasExtra(C.EXT if (data != null && data.hasExtra(C.EXTRA_TOKENID_LIST)) { List tokenData = data.getParcelableArrayListExtra(C.EXTRA_TOKENID_LIST); - ((ActivityFragment) getFragment(ACTIVITY)).addedToken(tokenData); + getFragment(ACTIVITY).addedToken(tokenData); + } + else if (data != null && data.getBooleanExtra(RESET_WALLET, false)) + { + viewModel.restartTokensService(); + //trigger wallet adapter reset + resetTokens(); } break; default: @@ -1219,13 +1063,6 @@ protected boolean onPrepareOptionsPanel(View view, Menu menu) return super.onPrepareOptionsPanel(view, menu); } - public static void setUpdatePrompt() - { - //TODO: periodically check this value (eg during page flipping) - //Set alert to user to update their app - updatePrompt = true; - } - void postponeWalletBackupWarning(String walletAddress) { removeSettingsBadgeKey(C.KEY_NEEDS_BACKUP); @@ -1237,7 +1074,7 @@ public void onBackPressed() //Check if current page is WALLET or not if (viewPager.getCurrentItem() == DAPP_BROWSER.ordinal()) { - ((DappBrowserFragment) getFragment(DAPP_BROWSER)).backPressed(); + getFragment(DAPP_BROWSER).backPressed(); } else if (viewPager.getCurrentItem() != WALLET.ordinal() && isNavBarVisible()) { @@ -1249,11 +1086,6 @@ else if (viewPager.getCurrentItem() != WALLET.ordinal() && isNavBarVisible()) } } - public void useActionSheet(String mode) - { - viewModel.actionSheetConfirm(mode); - } - private void hideSystemUI() { WindowCompat.setDecorFitsSystemWindows(getWindow(), false); @@ -1279,7 +1111,7 @@ private void checkIntents(String importData, String importPath, Intent startInte { importData = importData.substring(NotificationService.AWSTARTUP.length()); //move window to token if found - ((WalletFragment) getFragment(WALLET)).setImportFilename(importData); + getFragment(WALLET).setImportFilename(importData); } else if (startIntent.getStringExtra("url") != null) { @@ -1295,6 +1127,7 @@ else if (importData != null && importData.length() > 22 && importData.contains(A { Intent intent = new Intent(this, ApiV1Activity.class); intent.putExtra(C.Key.API_V1_REQUEST_URL, importData); + viewModel.track(Analytics.Action.DEEP_LINK_API_V1); startActivity(intent); return; } @@ -1306,6 +1139,7 @@ else if (importData != null && importData.length() > 22 && importData.contains(A String link = importData.substring(directLinkIndex + AW_MAGICLINK_DIRECT.length()); if (getSupportFragmentManager().getFragments().size() >= DAPP_BROWSER.ordinal()) { + viewModel.track(Analytics.Action.DEEP_LINK); showPage(DAPP_BROWSER); if (!dappFrag.isDetached()) dappFrag.loadDirect(link); } @@ -1348,4 +1182,95 @@ else if (importPath != null) Timber.tag("Intent").w(e); } } -} \ No newline at end of file + + @Override + public void signingComplete(SignatureFromKey signature, Signable message) + { + String signHex = Numeric.toHexString(signature.signature); + Timber.d("Initial Msg: %s", message.getMessage()); + awWalletConnectClient.signComplete(signature, message); + } + + @Override + public void signingFailed(Throwable error, Signable message) + { + awWalletConnectClient.signFail(error.getMessage(), message); + } + + @Override + public void getAuthorisation(SignAuthenticationCallback callback) + { + viewModelWC.getAuthenticationForSignature(viewModel.defaultWallet().getValue(), this, callback); + } + + @Override + public void sendTransaction(Web3Transaction tx) + { + + } + + @Override + public void dismissed(String txHash, long callbackId, boolean actionCompleted) + { + if (!actionCompleted) + { + awWalletConnectClient.dismissed(callbackId); + } + } + + @Override + public void notifyConfirm(String mode) + { + + } + + //TODO: Implement when passing transactions through here + ActivityResultLauncher getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> { //awWalletConnectClient.setCurrentGasIndex(result)); + }); + + @Override + public ActivityResultLauncher gasSelectLauncher() + { + return getGasSettings; + } + + private static class ScreenSlidePagerAdapter extends FragmentStateAdapter + { + public ScreenSlidePagerAdapter(@NonNull FragmentActivity fragmentActivity) + { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) + { + switch (WalletPage.values()[position]) + { + case WALLET: + default: + return new WalletFragment(); + case ACTIVITY: + return new ActivityFragment(); + case DAPP_BROWSER: + if (CustomViewSettings.hideDappBrowser()) + { + return new BaseFragment(); + } + else + { + return new DappBrowserFragment(); + } + case SETTINGS: + return new NewSettingsFragment(); + } + } + + @Override + public int getItemCount() + { + return WalletPage.values().length; + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/ImportKeystoreFragment.java b/app/src/main/java/com/alphawallet/app/ui/ImportKeystoreFragment.java index cd10b316be..3adb5ceb1e 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ImportKeystoreFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/ImportKeystoreFragment.java @@ -75,7 +75,10 @@ private void setupView() public void onResume() { super.onResume(); - if (keystore == null && getActivity() != null) setupView(); + if (keystore == null || password == null) + { + requireActivity().recreate(); + } } @Override @@ -93,11 +96,7 @@ private void handleKeypress(View view) { if (password.getVisibility() == View.GONE) { - keystore.setVisibility(View.GONE); - password.setVisibility(View.VISIBLE); - passwordText.setVisibility(View.VISIBLE); - password.requestFocus(); - updateButtonState(false); + showPassword(); } else { @@ -123,13 +122,9 @@ public String getPassword() public boolean backPressed() { - if (password != null && password.getVisibility() == View.VISIBLE) + if (password.getVisibility() == View.VISIBLE) { - keystore.setVisibility(View.VISIBLE); - password.setVisibility(View.GONE); - passwordText.setVisibility(View.GONE); - keystore.requestFocus(); - updateButtonState(true); + showKeystore(); return true; } else @@ -138,6 +133,24 @@ public boolean backPressed() } } + public void showKeystore() + { + keystore.setVisibility(View.VISIBLE); + password.setVisibility(View.GONE); + passwordText.setVisibility(View.GONE); + keystore.requestFocus(); + updateButtonState(true); + } + + private void showPassword() + { + keystore.setVisibility(View.GONE); + password.setVisibility(View.VISIBLE); + passwordText.setVisibility(View.VISIBLE); + password.requestFocus(); + updateButtonState(false); + } + public void setOnImportKeystoreListener(@Nullable OnImportKeystoreListener onImportKeystoreListener) { this.onImportKeystoreListener = onImportKeystoreListener == null ? dummyOnImportKeystoreListener diff --git a/app/src/main/java/com/alphawallet/app/ui/ImportSeedFragment.java b/app/src/main/java/com/alphawallet/app/ui/ImportSeedFragment.java index e88c59c334..8c9182b645 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ImportSeedFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/ImportSeedFragment.java @@ -36,7 +36,8 @@ @AndroidEntryPoint public class ImportSeedFragment extends ImportFragment implements OnSuggestionClickListener { private static final OnImportSeedListener dummyOnImportSeedListener = (s, c) -> {}; - private static final String validator = "[^a-z^A-Z^ ]"; + + public static final String validator = "[^a-z^A-Z^ ]"; private PasswordInputView seedPhrase; private Button importButton; diff --git a/app/src/main/java/com/alphawallet/app/ui/ImportTokenActivity.java b/app/src/main/java/com/alphawallet/app/ui/ImportTokenActivity.java index e748601473..de73fdb307 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ImportTokenActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/ImportTokenActivity.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import android.view.MenuItem; import android.view.View; import android.webkit.WebView; @@ -43,13 +42,11 @@ import java.math.BigDecimal; -import javax.inject.Inject; - import timber.log.Timber; import static com.alphawallet.app.C.IMPORT_STRING; import static com.alphawallet.app.entity.Operation.SIGN_DATA; -import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.GNOSIS_ID; import static com.alphawallet.token.tools.Convert.getEthString; import static com.alphawallet.token.tools.ParseMagicLink.currencyLink; import static com.alphawallet.token.tools.ParseMagicLink.spawnable; @@ -157,7 +154,7 @@ private void checkContractNetwork(String contractAddress) { case currencyLink: //for currency drop link, check xDai first, then other networks - viewModel.switchNetwork(XDAI_ID); + viewModel.switchNetwork(GNOSIS_ID); viewModel.checkTokenNetwork(contractAddress, "requiredPrefix"); break; default: diff --git a/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java b/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java index 346ce9ac7e..99cb584aaa 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java @@ -1,11 +1,13 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.C.ErrorCode.ALREADY_ADDED; +import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -21,16 +23,20 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.EIP681Type; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.ImportWalletCallback; +import com.alphawallet.app.entity.analytics.ImportWalletType; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.QRResult; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.analytics.QrScanSource; import com.alphawallet.app.entity.cryptokeys.KeyEncodingType; import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.service.KeyService; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.OnImportKeystoreListener; import com.alphawallet.app.ui.widget.OnImportPrivateKeyListener; import com.alphawallet.app.ui.widget.OnImportSeedListener; @@ -56,39 +62,41 @@ import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import static com.alphawallet.app.C.ErrorCode.ALREADY_ADDED; -import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; - @AndroidEntryPoint public class ImportWalletActivity extends BaseActivity implements OnImportSeedListener, ImportWalletCallback, OnImportKeystoreListener, OnImportPrivateKeyListener { - private enum ImportType - { - SEED_FORM_INDEX, KEYSTORE_FORM_INDEX, PRIVATE_KEY_FORM_INDEX, WATCH_FORM_INDEX - } - private final List> pages = new ArrayList<>(); - - ImportWalletViewModel importWalletViewModel; + ImportWalletViewModel viewModel; private AWalletAlertDialog dialog; private ImportType currentPage; + ActivityResultLauncher getQRCode = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> handleScanQR(result.getResultCode(), result.getData())); @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) + { super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + setContentView(R.layout.activity_import_wallet); toolbar(); + setTitle(getString(R.string.title_import)); + initViews(); + + initViewModel(); + } + + private void initViews() + { currentPage = ImportType.SEED_FORM_INDEX; String receivedState = getIntent().getStringExtra(C.EXTRA_STATE); boolean isWatch = receivedState != null && receivedState.equals("watch"); @@ -102,14 +110,17 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { viewPager.setAdapter(new TabPagerAdapter(this, pages)); viewPager.setOffscreenPageLimit(pages.size()); - viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() + { @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { super.onPageScrolled(position, positionOffset, positionOffsetPixels); } @Override - public void onPageSelected(int position) { + public void onPageSelected(int position) + { super.onPageSelected(position); int oldPos = currentPage.ordinal(); currentPage = ImportType.values()[position]; @@ -117,7 +128,8 @@ public void onPageSelected(int position) { } @Override - public void onPageScrollStateChanged(int state) { + public void onPageScrollStateChanged(int state) + { super.onPageScrollStateChanged(state); } }); @@ -140,15 +152,17 @@ public void onPageScrollStateChanged(int state) { viewPager.setUserInputEnabled(true); setTitle(getString(R.string.title_import)); } + } - importWalletViewModel = new ViewModelProvider(this) + private void initViewModel() + { + viewModel = new ViewModelProvider(this) .get(ImportWalletViewModel.class); - importWalletViewModel.progress().observe(this, this::onProgress); - importWalletViewModel.error().observe(this, this::onError); - importWalletViewModel.wallet().observe(this, this::onWallet); - importWalletViewModel.badSeed().observe(this, this::onBadSeed); - importWalletViewModel.watchExists().observe(this, this::onWatchExists); - + viewModel.progress().observe(this, this::onProgress); + viewModel.error().observe(this, this::onError); + viewModel.wallet().observe(this, this::onWallet); + viewModel.badSeed().observe(this, this::onBadSeed); + viewModel.watchExists().observe(this, this::onWatchExists); } private void handlePageChange(int oldPos, int position) @@ -184,8 +198,10 @@ private void onBadSeed(Boolean aBoolean) } @Override - protected void onResume() { + protected void onResume() + { super.onResume(); + viewModel.track(Analytics.Navigation.IMPORT_WALLET); ((ImportSeedFragment) pages.get(ImportType.SEED_FORM_INDEX.ordinal()).second) .setOnImportSeedListener(this); @@ -197,10 +213,11 @@ protected void onResume() { if (pages.size() > ImportType.WATCH_FORM_INDEX.ordinal() && pages.get(ImportType.WATCH_FORM_INDEX.ordinal()) != null) { ((SetWatchWalletFragment) pages.get(ImportType.WATCH_FORM_INDEX.ordinal()).second) - .setOnSetWatchWalletListener(importWalletViewModel); + .setOnSetWatchWalletListener(viewModel); } - if ( getIntent().getStringExtra(C.EXTRA_QR_CODE) != null) { + if (getIntent().getStringExtra(C.EXTRA_QR_CODE) != null) + { // wait till import wallet fragment will be available new Handler().postDelayed(() -> handleScanQR(Activity.RESULT_OK, getIntent()), 500); } @@ -216,32 +233,43 @@ private void resetFragments() } @Override - protected void onPause() { + protected void onPause() + { super.onPause(); hideDialog(); - importWalletViewModel.resetSignDialog(); + viewModel.resetSignDialog(); } - private void onWallet(Wallet wallet) { + private void onWallet(Pair wallet) + { onProgress(false); + + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_WALLET_TYPE, wallet.first.type.toString()); + props.put(Analytics.PROPS_IMPORT_WALLET_TYPE, wallet.second); + viewModel.track(Analytics.Action.IMPORT_WALLET, props); + Intent result = new Intent(); - result.putExtra(C.Key.WALLET, wallet); + result.putExtra(C.Key.WALLET, wallet.first); setResult(RESULT_OK, result); finish(); } @Override - public void onBackPressed() { + public void onBackPressed() + { setResult(RESULT_CANCELED); super.onBackPressed(); } - private void onError(ErrorEnvelope errorEnvelope) { + private void onError(ErrorEnvelope errorEnvelope) + { hideDialog(); String message = TextUtils.isEmpty(errorEnvelope.message) ? getString(R.string.error_import) : errorEnvelope.message; - if (errorEnvelope.code == ALREADY_ADDED) { + if (errorEnvelope.code == ALREADY_ADDED) + { message = getString(R.string.error_already_added); } dialog = new AWalletAlertDialog(this); @@ -254,9 +282,11 @@ private void onError(ErrorEnvelope errorEnvelope) { resetFragments(); } - private void onProgress(boolean shouldShowProgress) { + private void onProgress(boolean shouldShowProgress) + { hideDialog(); - if (shouldShowProgress) { + if (shouldShowProgress) + { dialog = new AWalletAlertDialog(this); dialog.setTitle(R.string.title_dialog_handling); dialog.setProgressMode(); @@ -266,24 +296,26 @@ private void onProgress(boolean shouldShowProgress) { } } - private void hideDialog() { - if (dialog != null && dialog.isShowing()) { + private void hideDialog() + { + if (dialog != null && dialog.isShowing()) + { dialog.dismiss(); } } - ActivityResultLauncher getQRCode = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> handleScanQR(result.getResultCode(), result.getData())); - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home && currentPage ==ImportType.KEYSTORE_FORM_INDEX) + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == android.R.id.home && currentPage == ImportType.KEYSTORE_FORM_INDEX) { - if (((ImportKeystoreFragment) pages.get(ImportType.KEYSTORE_FORM_INDEX.ordinal()).second).backPressed()) return true; + if (((ImportKeystoreFragment) pages.get(ImportType.KEYSTORE_FORM_INDEX.ordinal()).second).backPressed()) + return true; } else if (item.getItemId() == R.id.action_scan) { - Intent intent = new Intent(this, QRScanner.class); + Intent intent = new Intent(this, QRScannerActivity.class); + intent.putExtra(QrScanSource.KEY, QrScanSource.IMPORT_WALLET_SCREEN.getValue()); getQRCode.launch(intent); } @@ -293,7 +325,7 @@ else if (item.getItemId() == R.id.action_scan) @Override public void onSeed(String seedPhrase, Activity ctx) { - importWalletViewModel.importHDWallet(seedPhrase, this, this); + viewModel.importHDWallet(seedPhrase, this, this); } @Override @@ -303,7 +335,7 @@ public void onKeystore(String keystore, String password) if (Utils.isAddressValid(address)) { onProgress(true); - importWalletViewModel.checkKeystorePassword(keystore, address, password) + viewModel.checkKeystorePassword(keystore, address, password) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> keyStoreValid(result, address), this::reportKeystoreError) @@ -333,14 +365,17 @@ private void keyStoreValid(Boolean result, String address) if (!result) { keyImportError(getString(R.string.invalid_keystore)); + //return back to the start + ImportKeystoreFragment importKeystoreFragment = (ImportKeystoreFragment) pages.get(ImportType.KEYSTORE_FORM_INDEX.ordinal()).second; + importKeystoreFragment.showKeystore(); } - else if (importWalletViewModel.keystoreExists(address)) + else if (viewModel.keystoreExists(address)) { queryReplaceWalletKeystore(address); } else { - importWalletViewModel.importKeystoreWallet(address, this, this); + viewModel.importKeystoreWallet(address, this, this); } } @@ -348,7 +383,7 @@ private void queryReplaceWalletKeystore(String address) { replaceWallet(address); dialog.setButtonListener(v -> { - importWalletViewModel.importKeystoreWallet(address, this, this); + viewModel.importKeystoreWallet(address, this, this); }); dialog.show(); } @@ -359,17 +394,18 @@ public void onPrivateKey(String privateKey) try { BigInteger key = new BigInteger(privateKey, 16); - if (!WalletUtils.isValidPrivateKey(privateKey)) throw new Exception(getString(R.string.invalid_private_key)); + if (!WalletUtils.isValidPrivateKey(privateKey)) + throw new Exception(getString(R.string.invalid_private_key)); ECKeyPair keypair = ECKeyPair.create(key); String address = Numeric.prependHexPrefix(Keys.getAddress(keypair)); - if (importWalletViewModel.keystoreExists(address)) + if (viewModel.keystoreExists(address)) { queryReplaceWalletPrivateKey(address); } else { - importWalletViewModel.importPrivateKeyWallet(address, this, this); + viewModel.importPrivateKeyWallet(address, this, this); } } catch (Exception e) @@ -382,7 +418,7 @@ private void queryReplaceWalletPrivateKey(String address) { replaceWallet(address); dialog.setButtonListener(v -> { - importWalletViewModel.importPrivateKeyWallet(address, this, this); + viewModel.importPrivateKeyWallet(address, this, this); }); dialog.show(); } @@ -400,22 +436,23 @@ public void walletValidated(String data, KeyEncodingType type, KeyService.Authen switch (type) { case SEED_PHRASE_KEY: - importWalletViewModel.onSeed(data, level); + viewModel.onSeed(data, level); break; case KEYSTORE_KEY: ImportKeystoreFragment importKeystoreFragment = (ImportKeystoreFragment) pages.get(ImportType.KEYSTORE_FORM_INDEX.ordinal()).second; - importWalletViewModel.onKeystore(importKeystoreFragment.getKeystore(), importKeystoreFragment.getPassword(), data, level); + viewModel.onKeystore(importKeystoreFragment.getKeystore(), importKeystoreFragment.getPassword(), data, level); break; case RAW_HEX_KEY: ImportPrivateKeyFragment importPrivateKeyFragment = (ImportPrivateKeyFragment) pages.get(ImportType.PRIVATE_KEY_FORM_INDEX.ordinal()).second; - importWalletViewModel.onPrivateKey(importPrivateKeyFragment.getPrivateKey(), data, level); + viewModel.onPrivateKey(importPrivateKeyFragment.getPrivateKey(), data, level); break; } } } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { super.onActivityResult(requestCode, resultCode, data); if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) @@ -423,11 +460,11 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Operation taskCode = Operation.values()[requestCode - SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS]; if (resultCode == RESULT_OK) { - importWalletViewModel.completeAuthentication(taskCode); + viewModel.completeAuthentication(taskCode); } else { - importWalletViewModel.failedAuthentication(taskCode); + viewModel.failedAuthentication(taskCode); } } } @@ -437,11 +474,13 @@ private void handleScanQR(int resultCode, Intent data) switch (resultCode) { case Activity.RESULT_OK: - if (data != null) { + if (data != null) + { String barcode = data.getStringExtra(C.EXTRA_QR_CODE); //if barcode is still null, ensure we don't GPF - if (barcode == null) { + if (barcode == null) + { displayScanError(); return; } @@ -460,12 +499,12 @@ private void handleScanQR(int resultCode, Intent data) } } break; - case QRScanner.DENY_PERMISSION: + case QRScannerActivity.DENY_PERMISSION: showCameraDenied(); break; default: Timber.tag("SEND").e(String.format(getString(R.string.barcode_error_format), - "Code: " + resultCode + "Code: " + resultCode )); break; } @@ -507,11 +546,15 @@ private void displayScanError() dialog.show(); } - private String extractAddressFromStore(String store) { - try { + private String extractAddressFromStore(String store) + { + try + { JSONObject jsonObject = new JSONObject(store); return "0x" + Numeric.cleanHexPrefix(jsonObject.getString("address")); - } catch (JSONException ex) { + } + catch (JSONException ex) + { return null; } } @@ -546,4 +589,9 @@ private void onWatchExists(String address) }); dialog.show(); } + + private enum ImportType + { + SEED_FORM_INDEX, KEYSTORE_FORM_INDEX, PRIVATE_KEY_FORM_INDEX, WATCH_FORM_INDEX + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java b/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java index 0de37cd94c..e7637977d1 100644 --- a/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java @@ -10,14 +10,12 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; -import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.AddressMode; @@ -29,7 +27,7 @@ import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.ui.QRScanning.DisplayUtils; import com.alphawallet.app.ui.widget.entity.AmountReadyCallback; -import com.alphawallet.app.util.AWEnsResolver; +import com.alphawallet.app.util.ens.AWEnsResolver; import com.alphawallet.app.util.KeyboardUtils; import com.alphawallet.app.util.QRUtils; import com.alphawallet.app.viewmodel.MyAddressViewModel; @@ -41,8 +39,6 @@ import java.math.BigDecimal; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; diff --git a/app/src/main/java/com/alphawallet/app/ui/MyDappsFragment.java b/app/src/main/java/com/alphawallet/app/ui/MyDappsFragment.java index 100e2c55b9..423dba81b9 100644 --- a/app/src/main/java/com/alphawallet/app/ui/MyDappsFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/MyDappsFragment.java @@ -1,5 +1,7 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.ui.DappBrowserFragment.DAPP_CLICK; + import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; @@ -9,29 +11,29 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.DApp; import com.alphawallet.app.ui.widget.OnDappClickListener; import com.alphawallet.app.ui.widget.adapter.MyDappsListAdapter; import com.alphawallet.app.util.DappBrowserUtils; import com.alphawallet.app.util.KeyboardUtils; +import com.alphawallet.app.viewmodel.MyDappsViewModel; import com.alphawallet.app.widget.AWalletAlertDialog; import java.util.List; -import static com.alphawallet.app.ui.DappBrowserFragment.DAPP_CLICK; - -import timber.log.Timber; - import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; @AndroidEntryPoint -public class MyDappsFragment extends Fragment implements OnDappClickListener { +public class MyDappsFragment extends BaseFragment implements OnDappClickListener +{ + private MyDappsViewModel viewModel; private MyDappsListAdapter adapter; private AWalletAlertDialog dialog; private TextView noDapps; @@ -39,7 +41,8 @@ public class MyDappsFragment extends Fragment implements OnDappClickListener { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + @Nullable Bundle savedInstanceState) + { View view = inflater.inflate(R.layout.layout_my_dapps, container, false); adapter = new MyDappsListAdapter( getData(), @@ -53,21 +56,25 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c noDapps = view.findViewById(R.id.no_dapps); showOrHideViews(); KeyboardUtils.hideKeyboard(view); + viewModel = new ViewModelProvider(this).get(MyDappsViewModel.class); return view; } - private List getData() { + private List getData() + { return DappBrowserUtils.getMyDapps(getContext()); } - private void onDappEdited(DApp dapp) { + private void onDappEdited(DApp dapp) + { Intent intent = new Intent(getActivity(), AddEditDappActivity.class); intent.putExtra("mode", 1); intent.putExtra("dapp", dapp); getActivity().startActivity(intent); } - private void onDappRemoved(DApp dapp) { + private void onDappRemoved(DApp dapp) + { dialog = new AWalletAlertDialog(getActivity()); dialog.setTitle(R.string.title_remove_dapp); dialog.setMessage(getString(R.string.remove_from_my_dapps, dapp.getName())); @@ -81,7 +88,8 @@ private void onDappRemoved(DApp dapp) { dialog.show(); } - private void removeDapp(DApp dapp) { + private void removeDapp(DApp dapp) + { try { List myDapps = DappBrowserUtils.getMyDapps(getContext()); @@ -106,22 +114,29 @@ private void removeDapp(DApp dapp) { } } - private void updateData() { + private void updateData() + { adapter.setDapps(DappBrowserUtils.getMyDapps(getContext())); showOrHideViews(); } - private void showOrHideViews() { - if (adapter.getItemCount() > 0) { + private void showOrHideViews() + { + if (adapter.getItemCount() > 0) + { noDapps.setVisibility(View.GONE); - } else { + } + else + { noDapps.setVisibility(View.VISIBLE); } } @Override - public void onResume() { + public void onResume() + { super.onResume(); + viewModel.track(Analytics.Navigation.MY_DAPPS); updateData(); } diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java index cb359bdd31..fdc4700a8e 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java @@ -1,6 +1,8 @@ package com.alphawallet.app.ui; import static com.alphawallet.app.C.Key.WALLET; +import static com.alphawallet.app.C.SIGNAL_NFT_SYNC; +import static com.alphawallet.app.C.SYNC_STATUS; import android.content.Intent; import android.os.Bundle; @@ -86,7 +88,26 @@ protected void onCreate(@Nullable Bundle savedInstanceState) setupViewPager(); //check NFT events, expedite balance update + syncListener(); viewModel.checkEventsForToken(token); + viewModel.updateAttributes(token); + } + + private void syncListener() + { + getSupportFragmentManager() + .setFragmentResultListener(SIGNAL_NFT_SYNC, this, (requestKey, b) -> + { + CertifiedToolbarView certificateToolbar = findViewById(R.id.certified_toolbar); + if (!b.getBoolean(SYNC_STATUS, false)) + { + certificateToolbar.nftSyncComplete(); + } + else + { + certificateToolbar.showNFTSync(); + } + }); } private boolean hasTokenScriptOverride(Token t) @@ -199,6 +220,13 @@ private void setupViewPager() setupTabs(viewPager, pages); } + @Override + public void onResume() + { + super.onResume(); + if (assetsFragment == null) recreate(); + } + private void setupTabs(ViewPager2 viewPager, List> pages) { TabLayout tabLayout = findViewById(R.id.tab_layout); @@ -208,8 +236,6 @@ private void setupTabs(ViewPager2 viewPager, List> pages) TabUtils.decorateTabLayout(this, tabLayout); -// viewPager.setCurrentItem(1, true); - tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java index 5b411f2220..b99015b63a 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java @@ -1,5 +1,6 @@ package com.alphawallet.app.ui; +import static android.text.Html.FROM_HTML_MODE_LEGACY; import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; @@ -17,6 +18,7 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.view.menu.ActionMenuItemView; import androidx.lifecycle.ViewModelProvider; @@ -35,7 +37,6 @@ import com.alphawallet.app.service.GasService; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; import com.alphawallet.app.ui.widget.entity.NFTAttributeLayout; -import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.TokenFunctionViewModel; import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; @@ -47,6 +48,7 @@ import com.alphawallet.app.widget.TokenInfoView; import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.token.entity.TSAction; +import com.alphawallet.token.entity.TokenScriptResult.Attribute; import com.alphawallet.token.entity.XMLDsigDescriptor; import java.math.BigDecimal; @@ -57,7 +59,10 @@ import java.util.Map; import dagger.hilt.android.AndroidEntryPoint; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; @AndroidEntryPoint @@ -72,6 +77,7 @@ public class NFTAssetDetailActivity extends BaseActivity implements StandardFunc private AWalletAlertDialog dialog; private NFTImageView tokenImage; private NFTAttributeLayout nftAttributeLayout; + private NFTAttributeLayout tsAttributeLayout; private TextView tokenDescription; private ActionMenuItemView refreshMenu; private ProgressBar progressBar; @@ -90,6 +96,7 @@ public class NFTAssetDetailActivity extends BaseActivity implements StandardFunc private TokenInfoView tivLastSale; private TokenInfoView tivAveragePrice; private TokenInfoView tivFloorPrice; + private TokenInfoView tivRarityData; private Animation rotation; private ActivityResultLauncher handleTransactionSuccess; private ActivityResultLauncher getGasSettings; @@ -116,6 +123,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) setupFunctionBar(); updateDefaultTokenData(); + + viewModel.updateLocalAttributes(token, tokenId); } private void initIntents() @@ -148,6 +157,11 @@ public void onResume() viewModel.prepare(); viewModel.getAsset(token, tokenId); progressBar.setVisibility(View.VISIBLE); + tokenImage.onResume(); + } + else + { + recreate(); } } @@ -156,10 +170,18 @@ protected void onDestroy() { viewModel.onDestroy(); super.onDestroy(); + tokenImage.onDestroy(); } @Override - public boolean onCreateOptionsMenu(Menu menu) + public void onPause() + { + super.onPause(); + tokenImage.onPause(); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { getMenuInflater().inflate(R.menu.menu_refresh, menu); return super.onCreateOptionsMenu(menu); @@ -179,6 +201,7 @@ private void initViews() { tokenImage = findViewById(R.id.asset_image); nftAttributeLayout = findViewById(R.id.attributes); + tsAttributeLayout = findViewById(R.id.ts_attributes); tokenDescription = findViewById(R.id.token_description); descriptionLabel = findViewById(R.id.label_description); progressBar = findViewById(R.id.progress); @@ -196,6 +219,7 @@ private void initViews() tivLastSale = findViewById(R.id.last_sale); tivAveragePrice = findViewById(R.id.average_price); tivFloorPrice = findViewById(R.id.floor_price); + tivRarityData = findViewById(R.id.rarity); rotation = AnimationUtils.loadAnimation(this, R.anim.rotate_refresh); rotation.setRepeatCount(Animation.INFINITE); @@ -308,7 +332,7 @@ private void updateDefaultTokenData() tivNetwork.setValue(token.getNetworkName()); - tivContractAddress.setValue(Utils.formatAddress(token.tokenInfo.address)); + tivContractAddress.setCopyableValue(token.tokenInfo.address); } private void loadAssetFromMetadata(NFTAsset asset) @@ -324,9 +348,31 @@ private void loadAssetFromMetadata(NFTAsset asset) clearRefreshAnimation(); loadFromOpenSeaData(asset.getOpenSeaAsset()); + + final List attrs = new ArrayList<>(); + + if (viewModel.hasTokenScript(token)) + { + viewModel.getAssetDefinitionService().resolveAttrs(token, new ArrayList<>(Collections.singleton(tokenId)), null) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(attrs::add, this::onError, () -> showTSAttributes(attrs)) + .isDisposed(); + } } } + private void showTSAttributes(List attrs) + { + //should have resolved all the attrs + tsAttributeLayout.bindTSAttributes(attrs); + } + + private void onError(Throwable throwable) + { + Timber.w(throwable); + } + private void updateTokenImage(NFTAsset asset) { if (triggeredReload) tokenImage.clearImage(); @@ -367,7 +413,7 @@ private void updateDescription(String description) if (!TextUtils.isEmpty(description)) { descriptionLabel.setVisibility(View.VISIBLE); - tokenDescription.setText(Html.fromHtml(description)); + tokenDescription.setText(Html.fromHtml(description, FROM_HTML_MODE_LEGACY)); } } @@ -407,6 +453,11 @@ private void loadFromOpenSeaData(OpenSeaAsset openSeaAsset) nftAttributeLayout.bind(token, openSeaAsset.traits, 0); } + if (openSeaAsset.rarity != null && openSeaAsset.rarity.rank > 0) + { + tivRarityData.setValue("#" + openSeaAsset.rarity.rank); + } + if (openSeaAsset.owner != null && openSeaAsset.owner.user != null) { @@ -484,6 +535,7 @@ public void handleTokenScriptFunction(String function, List selectio { //does the function have a view? If it's transaction only then handle here Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + if (functions == null) return; TSAction action = functions.get(function); token.clearResultMap(); diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java index ba72bd56f1..0b8e9b8fba 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java @@ -2,6 +2,8 @@ import static android.app.Activity.RESULT_OK; +import static com.alphawallet.app.C.SIGNAL_NFT_SYNC; +import static com.alphawallet.app.C.SYNC_STATUS; import android.content.Intent; import android.os.Bundle; @@ -98,7 +100,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat searchLayout = view.findViewById(R.id.layout_search_tokens); - gridItemDecoration = new ItemOffsetDecoration(recyclerView.getContext(), R.dimen.grid_divider_offset); + gridItemDecoration = new ItemOffsetDecoration(requireContext(), R.dimen.grid_divider_offset); if (hasTokenScriptOverride(token)) { @@ -116,18 +118,18 @@ public void onAssetClicked(Pair item) { if (item.second.isCollection()) { - handleTransactionSuccess.launch(viewModel.showAssetListDetails(getContext(), wallet, token, item.second)); + handleTransactionSuccess.launch(viewModel.showAssetListDetails(requireContext(), wallet, token, item.second)); } else { - handleTransactionSuccess.launch(viewModel.showAssetDetails(getContext(), wallet, token, item.first)); + handleTransactionSuccess.launch(viewModel.showAssetDetails(requireContext(), wallet, token, item.first)); } } @Override public void onTokenClick(View view, Token token, List tokenIds, boolean selected) { - handleTransactionSuccess.launch(viewModel.showAssetDetails(getContext(), wallet, token, tokenIds.get(0))); + handleTransactionSuccess.launch(viewModel.showAssetDetails(requireContext(), wallet, token, tokenIds.get(0))); } @Override @@ -143,15 +145,15 @@ public void updateToken(Token newToken) public void showGridView() { - recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + recyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); recyclerView.addItemDecoration(gridItemDecoration); - recyclerView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.surface)); + recyclerView.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.surface)); initAndAttachAdapter(true); } public void showListView() { - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); recyclerView.removeItemDecoration(gridItemDecoration); recyclerView.setPadding(0, 0, 0, 0); initAndAttachAdapter(false); @@ -162,16 +164,25 @@ private void initAndAttachAdapter(boolean isGridView) if (hasTokenScriptOverride(token)) { searchLayout.setVisibility(View.GONE); - adapter = new NonFungibleTokenAdapter(this, token, viewModel.getAssetDefinitionService(), viewModel.getOpenseaService(), getActivity(), isGridView); + adapter = new NonFungibleTokenAdapter(this, token, viewModel.getAssetDefinitionService(), viewModel.getOpenseaService(), isGridView); } else { searchLayout.setVisibility(View.VISIBLE); - adapter = new NFTAssetsAdapter(getActivity(), token, this, isGridView); + adapter = new NFTAssetsAdapter(getActivity(), token, this, viewModel.getOpenseaService(), isGridView); search.addTextChangedListener(setupTextWatcher((NFTAssetsAdapter)adapter)); } recyclerView.setAdapter(adapter); + checkSyncStatus(); + } + + private void checkSyncStatus() + { + if (token == null || token.getTokenAssets() == null) return; + Bundle result = new Bundle(); + result.putBoolean(SYNC_STATUS, token.getTokenCount() != token.getTokenAssets().size()); + getParentFragmentManager().setFragmentResult(SIGNAL_NFT_SYNC, result); } private boolean hasTokenScriptOverride(Token t) diff --git a/app/src/main/java/com/alphawallet/app/ui/NameThisWalletActivity.java b/app/src/main/java/com/alphawallet/app/ui/NameThisWalletActivity.java index a5068eaf95..1b26957bc6 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NameThisWalletActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NameThisWalletActivity.java @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.viewmodel.NameThisWalletViewModel; @@ -99,6 +100,7 @@ private void checkENSName() protected void onResume() { super.onResume(); + viewModel.track(Analytics.Navigation.NAME_WALLET); viewModel.prepare(); } diff --git a/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java b/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java index 322b2c2e8c..5e2589fa60 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java @@ -16,6 +16,7 @@ import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_CURRENT_SCHEMA; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; @@ -36,14 +37,15 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.BackupOperationType; import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.Wallet; -import com.alphawallet.app.entity.WalletPage; import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.util.LocaleUtils; import com.alphawallet.app.util.UpdateUtils; +import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.NewSettingsViewModel; import com.alphawallet.app.widget.NotificationView; import com.alphawallet.app.widget.SettingsItemView; @@ -58,60 +60,6 @@ @AndroidEntryPoint public class NewSettingsFragment extends BaseFragment { - ActivityResultLauncher handleBackupClick = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - String keyBackup = ""; - boolean noLockScreen = false; - Intent data = result.getData(); - if (data != null) keyBackup = data.getStringExtra("Key"); - if (data != null) noLockScreen = data.getBooleanExtra("nolock", false); - - Bundle b = new Bundle(); - b.putBoolean(C.HANDLE_BACKUP, result.getResultCode() == RESULT_OK); - b.putString("Key", keyBackup); - b.putBoolean("nolock", noLockScreen); - getParentFragmentManager().setFragmentResult(C.HANDLE_BACKUP, b); - }); - - ActivityResultLauncher networkSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - //send instruction to restart tokenService - getParentFragmentManager().setFragmentResult(RESET_TOKEN_SERVICE, new Bundle()); - }); - - ActivityResultLauncher advancedSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - Intent data = result.getData(); - if (data == null) return; - if (data.getBooleanExtra(RESET_WALLET, false)) - { - getParentFragmentManager().setFragmentResult(RESET_WALLET, new Bundle()); - } - else if (data.getBooleanExtra(CHANGE_CURRENCY, false)) - { - getParentFragmentManager().setFragmentResult(CHANGE_CURRENCY, new Bundle()); - } - else if (data.getBooleanExtra(CHANGED_LOCALE, false)) - { - getParentFragmentManager().setFragmentResult(CHANGED_LOCALE, new Bundle()); - } - }); - - ActivityResultLauncher updateLocale = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - updateLocale(result.getData()); - }); - - ActivityResultLauncher updateCurrency = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - updateCurrency(result.getData()); - }); - private NewSettingsViewModel viewModel; private LinearLayout walletSettingsLayout; private LinearLayout systemSettingsLayout; @@ -137,17 +85,18 @@ else if (data.getBooleanExtra(CHANGED_LOCALE, false)) private ImageView closeBtn; private NotificationView notificationView; private MaterialCardView updateLayout; - private int pendingUpdate = 0; + private String pendingUpdate; private Wallet wallet; + private ActivityResultLauncher handleBackupClick; + private ActivityResultLauncher networkSettingsHandler; + private ActivityResultLauncher advancedSettingsHandler; + private ActivityResultLauncher updateLocale; + private ActivityResultLauncher updateCurrency; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - viewModel = new ViewModelProvider(this) - .get(NewSettingsViewModel.class); - viewModel.defaultWallet().observe(getViewLifecycleOwner(), this::onDefaultWallet); - viewModel.backUpMessage().observe(getViewLifecycleOwner(), this::backupWarning); LocaleUtils.setActiveLocale(getContext()); View view = inflater.inflate(R.layout.fragment_settings, container, false); @@ -156,6 +105,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c setToolbarTitle(R.string.toolbar_header_settings); + initViewModel(); + initializeSettings(view); addSettingsToLayout(); @@ -166,13 +117,76 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c initNotificationView(view); - checkPendingUpdate(view); + initResultLaunchers(); getParentFragmentManager().setFragmentResult(SETTINGS_INSTANTIATED, new Bundle()); return view; } + private void initResultLaunchers() + { + handleBackupClick = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + String keyBackup = ""; + boolean noLockScreen = false; + Intent data = result.getData(); + if (data != null) keyBackup = data.getStringExtra("Key"); + if (data != null) noLockScreen = data.getBooleanExtra("nolock", false); + + Bundle b = new Bundle(); + b.putBoolean(C.HANDLE_BACKUP, result.getResultCode() == RESULT_OK); + b.putString("Key", keyBackup); + b.putBoolean("nolock", noLockScreen); + getParentFragmentManager().setFragmentResult(C.HANDLE_BACKUP, b); + }); + + networkSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + //send instruction to restart tokenService + getParentFragmentManager().setFragmentResult(RESET_TOKEN_SERVICE, new Bundle()); + }); + + advancedSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + Intent data = result.getData(); + if (data == null) return; + if (data.getBooleanExtra(RESET_WALLET, false)) + { + getParentFragmentManager().setFragmentResult(RESET_WALLET, new Bundle()); + } + else if (data.getBooleanExtra(CHANGE_CURRENCY, false)) + { + getParentFragmentManager().setFragmentResult(CHANGE_CURRENCY, new Bundle()); + } + else if (data.getBooleanExtra(CHANGED_LOCALE, false)) + { + getParentFragmentManager().setFragmentResult(CHANGED_LOCALE, new Bundle()); + } + }); + updateLocale = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + updateLocale(result.getData()); + }); + updateCurrency = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + updateCurrency(result.getData()); + }); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(NewSettingsViewModel.class); + viewModel.defaultWallet().observe(getViewLifecycleOwner(), this::onDefaultWallet); + viewModel.backUpMessage().observe(getViewLifecycleOwner(), this::backupWarning); + } + private void initNotificationView(View view) { notificationView = view.findViewById(R.id.notification); @@ -193,11 +207,37 @@ private void initNotificationView(View view) } } - public void signalUpdate(int updateVersion) + @Override + public void signalPlayStoreUpdate(int updateVersion) { //add wallet update signal to adapter + pendingUpdate = String.valueOf(updateVersion); + checkPendingUpdate(getView(), true, v -> + { + UpdateUtils.pushUpdateDialog(getActivity()); + updateLayout.setVisibility(View.GONE); + pendingUpdate = ""; + if (getActivity() != null) + { + ((HomeActivity) getActivity()).removeSettingsBadgeKey(C.KEY_UPDATE_AVAILABLE); + } + }); + } + + @Override + public void signalExternalUpdate(String updateVersion) + { pendingUpdate = updateVersion; - checkPendingUpdate(getView()); + checkPendingUpdate(getView(), false, v -> + { + pendingUpdate = ""; + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(C.EXTERNAL_APP_DOWNLOAD_LINK)); + if (getActivity() != null) + { + ((HomeActivity) getActivity()).removeSettingsBadgeKey(C.KEY_UPDATE_AVAILABLE); + getActivity().startActivity(intent); + } + }); } private void initBackupWarningViews(View view) @@ -412,14 +452,10 @@ private void onDefaultWallet(Wallet wallet) this.wallet = wallet; if (wallet.address != null) { - if (!wallet.ENSname.isEmpty()) - { - changeWalletSetting.setSubtitle(wallet.ENSname + " | " + wallet.address); - } - else - { - changeWalletSetting.setSubtitle(wallet.address); - } + String walletAddressDisplay = wallet.ENSname.isEmpty() ? wallet.address + : wallet.ENSname + " | " + Utils.formatAddress(wallet.address); + + changeWalletSetting.setSubtitle(walletAddressDisplay); } switch (wallet.authLevel) @@ -476,14 +512,16 @@ public void onResume() super.onResume(); if (viewModel == null) { - ((HomeActivity) getActivity()).resetFragment(WalletPage.SETTINGS); + requireActivity().recreate(); } else { + viewModel.track(Analytics.Navigation.SETTINGS); viewModel.prepare(); } } + @Override public void backupSeedSuccess(boolean hasNoLock) { if (viewModel != null) viewModel.TestWalletBackup(); @@ -613,36 +651,33 @@ private void onSupportSettingClicked() private void onWalletConnectSettingClicked() { Intent intent = new Intent(getActivity(), WalletConnectSessionActivity.class); - intent.putExtra("wallet", wallet); startActivity(intent); } - private void checkPendingUpdate(View view) + private void checkPendingUpdate(View view, boolean isFromPlayStore, View.OnClickListener listener) { if (updateLayout == null || view == null) return; - if (pendingUpdate > 0) + if (!TextUtils.isEmpty(pendingUpdate)) { updateLayout.setVisibility(View.VISIBLE); + updateLayout.setOnClickListener(listener); TextView current = view.findViewById(R.id.text_detail_current); TextView available = view.findViewById(R.id.text_detail_available); - current.setText(getString(R.string.installed_version, String.valueOf(BuildConfig.VERSION_CODE))); + if (isFromPlayStore) + { + current.setText(getString(R.string.installed_version, String.valueOf(BuildConfig.VERSION_CODE))); + } + else + { + current.setText(getString(R.string.installed_version, BuildConfig.VERSION_NAME)); + } available.setText(getString(R.string.available_version, String.valueOf(pendingUpdate))); + if (getActivity() != null) { ((HomeActivity) getActivity()).addSettingsBadgeKey(C.KEY_UPDATE_AVAILABLE); } - - updateLayout.setOnClickListener(v -> - { - UpdateUtils.pushUpdateDialog(getActivity()); - updateLayout.setVisibility(View.GONE); - pendingUpdate = 0; - if (getActivity() != null) - { - ((HomeActivity) getActivity()).removeSettingsBadgeKey(C.KEY_UPDATE_AVAILABLE); - } - }); } else { diff --git a/app/src/main/java/com/alphawallet/app/ui/NodeStatusActivity.java b/app/src/main/java/com/alphawallet/app/ui/NodeStatusActivity.java index ebc99f76da..7ef5b6b8c8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NodeStatusActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NodeStatusActivity.java @@ -2,7 +2,6 @@ import android.os.Bundle; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import androidx.annotation.Nullable; @@ -15,7 +14,7 @@ import com.alphawallet.app.ui.widget.adapter.NodeStatusAdapter; import com.alphawallet.app.viewmodel.NodeStatusViewModel; import com.alphawallet.app.widget.ActionSheetDialog; -import com.alphawallet.app.widget.ActionSheetMode; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.app.widget.StandardHeader; import com.alphawallet.ethereum.NetworkInfo; diff --git a/app/src/main/java/com/alphawallet/app/ui/QRScanning/QRScanner.java b/app/src/main/java/com/alphawallet/app/ui/QRScanning/QRScannerActivity.java similarity index 80% rename from app/src/main/java/com/alphawallet/app/ui/QRScanning/QRScanner.java rename to app/src/main/java/com/alphawallet/app/ui/QRScanning/QRScannerActivity.java index 259e2cc20d..fbfc48d0c5 100644 --- a/app/src/main/java/com/alphawallet/app/ui/QRScanning/QRScanner.java +++ b/app/src/main/java/com/alphawallet/app/ui/QRScanning/QRScannerActivity.java @@ -1,5 +1,10 @@ package com.alphawallet.app.ui.QRScanning; +import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.os.Build.VERSION.SDK_INT; +import static androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; +import static com.alphawallet.app.repository.SharedPreferenceRepository.FULL_SCREEN_STATE; + import android.Manifest; import android.app.Activity; import android.content.Intent; @@ -14,7 +19,6 @@ import android.text.TextUtils; import android.view.KeyEvent; import android.widget.TextView; -import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -24,12 +28,19 @@ import androidx.core.view.WindowInsetsControllerCompat; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.OnLifecycleEvent; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; +import com.alphawallet.app.entity.analytics.QrScanSource; import com.alphawallet.app.ui.BaseActivity; import com.alphawallet.app.ui.WalletConnectActivity; +import com.alphawallet.app.ui.WalletConnectV2Activity; +import com.alphawallet.app.walletconnect.util.WalletConnectHelper; +import com.alphawallet.app.viewmodel.QrScannerViewModel; import com.alphawallet.app.widget.AWalletAlertDialog; import com.google.zxing.BarcodeFormat; import com.google.zxing.BinaryBitmap; @@ -49,22 +60,24 @@ import java.util.Collection; import java.util.List; +import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import static android.Manifest.permission.READ_EXTERNAL_STORAGE; -import static android.os.Build.VERSION.SDK_INT; -import static androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; -import static com.alphawallet.app.repository.SharedPreferenceRepository.FULL_SCREEN_STATE; - /** * Created by JB on 12/09/2021. */ -public class QRScanner extends BaseActivity +@AndroidEntryPoint +public class QRScannerActivity extends BaseActivity { + public static final int RC_HANDLE_IMAGE_PICKUP = 3; + public static final int DENY_PERMISSION = 1; + public static final int WALLET_CONNECT = 2; + private static final int RC_HANDLE_CAMERA_PERM = 2; + private QrScannerViewModel viewModel; private DecoratedBarcodeView barcodeView; private BeepManager beepManager; private String lastText; @@ -74,23 +87,13 @@ public class QRScanner extends BaseActivity private TextView myAddressButton; private TextView browseButton; private boolean torchOn = false; - - private static final int RC_HANDLE_CAMERA_PERM = 2; - public static final int RC_HANDLE_IMAGE_PICKUP = 3; - public static final int DENY_PERMISSION = 1; - public static final int WALLET_CONNECT = 2; + private ActivityResultLauncher getQRImage; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) - { - Toast.makeText(this, R.string.toast_qr_scanning_requires_api_24, Toast.LENGTH_SHORT).show(); - finish(); - } - hideSystemUI(); int rc = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA); @@ -102,6 +105,25 @@ protected void onCreate(Bundle savedInstanceState) { requestCameraPermission(); } + + initResultLaunchers(); + + initViewModel(); + } + + private void initResultLaunchers() + { + getQRImage = registerForActivityResult(new ActivityResultContracts.GetContent(), + uri -> disposable = concertAndHandle(uri) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onSuccess, this::onError)); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(QrScannerViewModel.class); } private void initView() @@ -113,6 +135,31 @@ private void initView() Collection formats = Arrays.asList(BarcodeFormat.QR_CODE, BarcodeFormat.CODE_39, BarcodeFormat.AZTEC); barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(formats)); barcodeView.initializeFromIntent(getIntent()); + BarcodeCallback callback = new BarcodeCallback() + { + @Override + public void barcodeResult(BarcodeResult result) + { + if (result.getText() == null || result.getText().equals(lastText)) + { + // Prevent duplicate scans + return; + } + + lastText = result.getText(); + barcodeView.setStatusText(result.getText()); + + beepManager.playBeepSoundAndVibrate(); + + //send result + handleQRCode(result.getText()); + } + + @Override + public void possibleResultPoints(List resultPoints) + { + } + }; barcodeView.decodeContinuous(callback); barcodeView.setStatusText(""); @@ -127,7 +174,8 @@ private void initView() setupButtons(); } - private void setupToolbar() { + private void setupToolbar() + { toolbar(); setTitle(getString(R.string.action_scan_dapp)); enableDisplayHomeAsUp(R.drawable.ic_close); @@ -140,12 +188,12 @@ private void setupButtons() { if (!torchOn) { - flashButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_flash_off, 0,0); + flashButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_flash_off, 0, 0); barcodeView.setTorchOn(); } else { - flashButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_flash, 0,0); + flashButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_flash, 0, 0); barcodeView.setTorchOff(); } torchOn = !torchOn; @@ -177,51 +225,23 @@ private void setupButtons() } @Override - protected void onResume() { + protected void onResume() + { super.onResume(); + String source = getIntent().getStringExtra(QrScanSource.KEY); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_QR_SCAN_SOURCE, source); + viewModel.track(Analytics.Navigation.SCAN_QR_CODE, props); if (barcodeView != null) barcodeView.resume(); } @Override - protected void onPause() { + protected void onPause() + { super.onPause(); if (barcodeView != null) barcodeView.pause(); } - private final BarcodeCallback callback = new BarcodeCallback() - { - @Override - public void barcodeResult(BarcodeResult result) - { - if (result.getText() == null || result.getText().equals(lastText)) - { - // Prevent duplicate scans - return; - } - - lastText = result.getText(); - barcodeView.setStatusText(result.getText()); - - beepManager.playBeepSoundAndVibrate(); - - //send result - handleQRCode(result.getText()); - } - - @Override - public void possibleResultPoints(List resultPoints) - { - } - }; - - ActivityResultLauncher getQRImage = registerForActivityResult(new ActivityResultContracts.GetContent(), - uri -> { - disposable = concertAndHandle(uri) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onSuccess, this::onError); - }); - private void onError(Throwable throwable) { displayErrorDialog(getString(R.string.title_dialog_error), getString(R.string.error_browse_selection)); @@ -296,7 +316,7 @@ private Bitmap getMutableCapturedImage(Uri selectedPhotoUri) // Handles the requesting of the camera permission. private void requestCameraPermission() { - Timber.tag("QR SCanner").w("Camera permission is not granted. Requesting permission"); + Timber.tag("QR SCanner").w("Camera permission is not granted. Requesting permission"); final String[] permissions = new String[]{Manifest.permission.CAMERA}; ActivityCompat.requestPermissions(this, permissions, RC_HANDLE_CAMERA_PERM); //always ask for permission to scan @@ -344,6 +364,8 @@ else if (requestCode == RC_HANDLE_IMAGE_PICKUP) private void displayErrorDialog(String title, String errorMessage) { + viewModel.track(Analytics.Action.SCAN_QR_CODE_ERROR); + AWalletAlertDialog aDialog = new AWalletAlertDialog(this); aDialog.setTitle(title); aDialog.setMessage(errorMessage); @@ -357,9 +379,18 @@ private void displayErrorDialog(String title, String errorMessage) private void startWalletConnect(String qrCode) { - Intent intent = new Intent(this, WalletConnectActivity.class); - intent.putExtra("qrCode", qrCode); - intent.putExtra(C.EXTRA_CHAIN_ID, chainIdOverride); + Intent intent; + if (WalletConnectHelper.isWalletConnectV1(qrCode)) + { + intent = new Intent(this, WalletConnectActivity.class); + intent.putExtra("qrCode", qrCode); + intent.putExtra(C.EXTRA_CHAIN_ID, chainIdOverride); + } + else + { + intent = new Intent(this, WalletConnectV2Activity.class); + intent.putExtra("url", qrCode); + } startActivity(intent); setResult(WALLET_CONNECT); finish(); @@ -368,13 +399,19 @@ private void startWalletConnect(String qrCode) @Override public void onBackPressed() { + viewModel.track(Analytics.Action.SCAN_QR_CODE_CANCELLED); Intent intent = new Intent(); setResult(Activity.RESULT_CANCELED, intent); finish(); } @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { + public boolean onKeyDown(int keyCode, KeyEvent event) + { + if (barcodeView == null) + { + return false; + } return barcodeView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); } @@ -395,6 +432,8 @@ public void handleQRCode(String qrCode) } else { + viewModel.track(Analytics.Action.SCAN_QR_CODE_SUCCESS); + Intent intent = new Intent(); intent.putExtra(C.EXTRA_QR_CODE, qrCode); setResult(Activity.RESULT_OK, intent); diff --git a/app/src/main/java/com/alphawallet/app/ui/RedeemSignatureDisplayActivity.java b/app/src/main/java/com/alphawallet/app/ui/RedeemSignatureDisplayActivity.java index 4a46e64142..c8240104b3 100644 --- a/app/src/main/java/com/alphawallet/app/ui/RedeemSignatureDisplayActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/RedeemSignatureDisplayActivity.java @@ -1,7 +1,10 @@ package com.alphawallet.app.ui; -import androidx.lifecycle.ViewModelProvider; -import androidx.lifecycle.ViewModelProviders; +import static com.alphawallet.app.C.Key.TICKET_RANGE; +import static com.alphawallet.app.C.Key.WALLET; +import static com.alphawallet.app.C.PRUNE_ACTIVITY; +import static com.alphawallet.app.entity.Operation.SIGN_DATA; + import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -9,9 +12,6 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - import android.view.View; import android.webkit.WebView; import android.widget.ImageView; @@ -19,38 +19,32 @@ import android.widget.TextView; import android.widget.Toast; -import com.alphawallet.app.BuildConfig; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import com.alphawallet.app.C; -import com.alphawallet.app.viewmodel.RedeemAssetSelectViewModel; -import com.alphawallet.app.web3.Web3TokenView; -import com.alphawallet.app.web3.entity.PageReadyCallback; -import com.alphawallet.ethereum.EthereumNetworkBase; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.common.BitMatrix; -import com.journeyapps.barcodescanner.BarcodeEncoder; +import com.alphawallet.app.R; import com.alphawallet.app.entity.FinishReceiver; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.SignaturePair; -import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.ui.widget.entity.TicketRangeParcel; - -import timber.log.Timber; - -import com.alphawallet.app.R; - import com.alphawallet.app.viewmodel.RedeemSignatureDisplayModel; +import com.alphawallet.app.web3.Web3TokenView; +import com.alphawallet.app.web3.entity.PageReadyCallback; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.SignTransactionDialog; - -import javax.inject.Inject; - -import static com.alphawallet.app.C.Key.*; -import static com.alphawallet.app.C.PRUNE_ACTIVITY; -import static com.alphawallet.app.entity.Operation.SIGN_DATA; +import com.alphawallet.ethereum.EthereumNetworkBase; +import com.alphawallet.token.entity.ViewType; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.common.BitMatrix; +import com.journeyapps.barcodescanner.BarcodeEncoder; import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; /** * Created by James on 24/01/2018. @@ -108,7 +102,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { //given a webview populate with rendered token tokenView.displayTicketHolder(token, ticketRange.range, viewModel.getAssetDefinitionService()); tokenView.setOnReadyCallback(this); - tokenView.setLayout(token, true); + tokenView.setLayout(token, ViewType.ITEM_VIEW); finishReceiver = new FinishReceiver(this); } @@ -281,4 +275,4 @@ public void onPageRendered(WebView view) { webWrapper.setVisibility(View.VISIBLE); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/ui/ScammerWarningActivity.java b/app/src/main/java/com/alphawallet/app/ui/ScammerWarningActivity.java index 0e3aea6db1..80fac01b2d 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ScammerWarningActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/ScammerWarningActivity.java @@ -61,12 +61,14 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint -public class ScammerWarningActivity extends BaseActivity { +public class ScammerWarningActivity extends BaseActivity +{ private FunctionButtonBar functionButtonBar; private Wallet wallet; @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) + { super.onCreate(savedInstanceState); lockOrientation(); toolbar(); @@ -75,15 +77,20 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } @SuppressLint("SourceLockedOrientationActivity") - private void lockOrientation() { - if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + private void lockOrientation() + { + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) + { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } else { + } + else + { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } - private void setShowSeedPhraseSplash() { + private void setShowSeedPhraseSplash() + { setContentView(R.layout.activity_show_seed); initViews(); functionButtonBar.setPrimaryButtonText(R.string.show_seed_phrase); @@ -92,14 +99,16 @@ private void setShowSeedPhraseSplash() { }); } - private void openBackupKeyActivity() { + private void openBackupKeyActivity() + { Intent intent = new Intent(this, BackupKeyActivity.class); intent.putExtra(WALLET, wallet); intent.putExtra("STATE", SHOW_SEED_PHRASE_SINGLE); startActivity(intent); } - private void initViews() { + private void initViews() + { functionButtonBar = findViewById(R.id.layoutButtons); getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); toolbar(); diff --git a/app/src/main/java/com/alphawallet/app/ui/SelectNetworkFilterActivity.java b/app/src/main/java/com/alphawallet/app/ui/SelectNetworkFilterActivity.java index 13d77c981e..f1c5a7eb67 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SelectNetworkFilterActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SelectNetworkFilterActivity.java @@ -1,11 +1,9 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.ui.AddCustomRPCNetworkActivity.CHAIN_ID; + import android.content.Intent; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.util.TypedValue; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.widget.CompoundButton; @@ -15,12 +13,10 @@ import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; -import com.alphawallet.app.C; import com.alphawallet.app.R; -import com.alphawallet.app.repository.EthereumNetworkRepositoryType; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.ui.widget.adapter.MultiSelectNetworkAdapter; import com.alphawallet.app.ui.widget.entity.NetworkItem; -import com.alphawallet.app.ui.widget.entity.WarningData; import com.alphawallet.app.viewmodel.SelectNetworkFilterViewModel; import com.alphawallet.app.widget.TestNetDialog; import com.alphawallet.ethereum.NetworkInfo; @@ -29,40 +25,31 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Inject; - - -import static com.alphawallet.app.ui.AddCustomRPCNetworkActivity.CHAIN_ID; - import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint -public class SelectNetworkFilterActivity extends SelectNetworkBaseActivity implements TestNetDialog.TestNetDialogCallback { +public class SelectNetworkFilterActivity extends SelectNetworkBaseActivity implements TestNetDialog.TestNetDialogCallback +{ private SelectNetworkFilterViewModel viewModel; - private MultiSelectNetworkAdapter mainNetAdapter; private MultiSelectNetworkAdapter testNetAdapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setTitle(getString(R.string.select_active_networks)); - viewModel = new ViewModelProvider(this) .get(SelectNetworkFilterViewModel.class); - setupList(); - initTestNetDialog(this); } @Override - protected void onResume() { + protected void onResume() + { super.onResume(); setupFilterList(); + viewModel.track(Analytics.Navigation.SELECT_NETWORKS); } void setupList() @@ -74,6 +61,10 @@ void setupList() CompoundButton.OnCheckedChangeListener mainnetListener = (compoundButton, checked) -> { testnetSwitch.setChecked(!checked); + if (checked) + { + updateTitle(mainNetAdapter.getSelectedItemCount()); + } }; CompoundButton.OnCheckedChangeListener testnetListener = (compoundButton, checked) -> @@ -87,6 +78,11 @@ void setupList() if (checked) { testnetDialog.show(); + updateTitle(testNetAdapter.getSelectedItemCount()); + } + else + { + updateTitle(mainNetAdapter.getSelectedItemCount()); } }; @@ -103,9 +99,11 @@ private void setupFilterList() List mainNetList = viewModel.getNetworkList(true); List testNetList = viewModel.getNetworkList(false); - MultiSelectNetworkAdapter.EditNetworkListener editNetworkListener = new MultiSelectNetworkAdapter.EditNetworkListener() { + MultiSelectNetworkAdapter.Callback callback = new MultiSelectNetworkAdapter.Callback() + { - private void showPopup(View view, long chainId) { + private void showPopup(View view, long chainId) + { LayoutInflater inflater = LayoutInflater.from(SelectNetworkFilterActivity.this); View popupView = inflater.inflate(R.layout.popup_view_delete_network, null); @@ -121,14 +119,17 @@ private void showPopup(View view, long chainId) { }); NetworkInfo network = viewModel.getNetworkByChain(chainId); - if (network.isCustom) { + if (network.isCustom) + { popupView.findViewById(R.id.popup_delete).setOnClickListener(v -> { // delete network viewModel.removeCustomNetwork(chainId); popupWindow.dismiss(); setupFilterList(); }); - } else { + } + else + { popupView.findViewById(R.id.popup_delete).setVisibility(View.GONE); } @@ -141,16 +142,30 @@ private void showPopup(View view, long chainId) { } @Override - public void onEditNetwork(long chainId, View parent) { + public void onEditSelected(long chainId, View parent) + { showPopup(parent, chainId); } + + @Override + public void onCheckChanged(long chainId, int count) + { + updateTitle(count); + } }; - mainNetAdapter = new MultiSelectNetworkAdapter(mainNetList, editNetworkListener); + mainNetAdapter = new MultiSelectNetworkAdapter(mainNetList, callback); mainnetRecyclerView.setAdapter(mainNetAdapter); - testNetAdapter = new MultiSelectNetworkAdapter(testNetList, editNetworkListener); + testNetAdapter = new MultiSelectNetworkAdapter(testNetList, callback); testnetRecyclerView.setAdapter(testNetAdapter); + + updateTitle(viewModel.mainNetActive() ? mainNetAdapter.getSelectedItemCount() : testNetAdapter.getSelectedItemCount()); + } + + private void updateTitle(int count) + { + setTitle(getString(R.string.title_enabled_networks, String.valueOf(count))); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/SelectRouteActivity.java b/app/src/main/java/com/alphawallet/app/ui/SelectRouteActivity.java new file mode 100644 index 0000000000..c29c1bfed2 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/SelectRouteActivity.java @@ -0,0 +1,201 @@ +package com.alphawallet.app.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.lifi.Route; +import com.alphawallet.app.ui.widget.adapter.RouteAdapter; +import com.alphawallet.app.ui.widget.entity.ProgressInfo; +import com.alphawallet.app.util.BalanceUtils; +import com.alphawallet.app.util.SwapUtils; +import com.alphawallet.app.viewmodel.SelectRouteViewModel; +import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.widget.AddressIcon; +import com.google.android.material.button.MaterialButton; + +import java.util.List; +import java.util.Locale; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class SelectRouteActivity extends BaseActivity +{ + private static final long GET_ROUTES_INTERVAL_MS = 30000; + private static final long COUNTDOWN_INTERVAL_MS = 1000; + private SelectRouteViewModel viewModel; + private RecyclerView recyclerView; + private TextView fromAmount; + private TextView fromSymbol; + private TextView currentPrice; + private TextView countdownText; + private LinearLayout noRoutesLayout; + private MaterialButton selectExchangesBtn; + private AddressIcon fromTokenIcon; + private AWalletAlertDialog progressDialog; + private CountDownTimer getRoutesTimer; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_select_route); + + toolbar(); + + setTitle(getString(R.string.title_select_route)); + + initViews(); + + initViewModel(); + + initTimer(); + } + + @Override + protected void onResume() + { + getRoutes(); + super.onResume(); + } + + @Override + protected void onPause() + { + if (getRoutesTimer != null) + { + getRoutesTimer.cancel(); + } + super.onPause(); + } + + private void initViews() + { + recyclerView = findViewById(R.id.list_routes); + fromAmount = findViewById(R.id.from_amount); + fromSymbol = findViewById(R.id.from_symbol); + fromTokenIcon = findViewById(R.id.from_token_icon); + currentPrice = findViewById(R.id.current_price); + countdownText = findViewById(R.id.text_countdown); + noRoutesLayout = findViewById(R.id.layout_no_routes_found); + selectExchangesBtn = findViewById(R.id.btn_select_exchanges); + selectExchangesBtn.setOnClickListener(v -> { + Intent intent = new Intent(this, SelectSwapProvidersActivity.class); + startActivity(intent); + }); + + progressDialog = new AWalletAlertDialog(this); + progressDialog.setCancelable(false); + progressDialog.setProgressMode(); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(SelectRouteViewModel.class); + viewModel.routes().observe(this, this::onRoutes); + viewModel.progressInfo().observe(this, this::onProgressInfo); + } + + private void initTimer() + { + getRoutesTimer = new CountDownTimer(GET_ROUTES_INTERVAL_MS, COUNTDOWN_INTERVAL_MS) + { + @Override + public void onTick(long millisUntilFinished) + { + String format = millisUntilFinished < 10000 ? "0:0%d" : "0:%d"; + String time = String.format(Locale.ENGLISH, format, millisUntilFinished / 1000); + countdownText.setText(getString(R.string.label_available_routes, time)); + } + + @Override + public void onFinish() + { + getRoutes(); + } + }; + } + + private void getRoutes() + { + String fromChainId = getIntent().getStringExtra("fromChainId"); + String toChainId = getIntent().getStringExtra("toChainId"); + String fromTokenAddress = getIntent().getStringExtra("fromTokenAddress"); + String toTokenAddress = getIntent().getStringExtra("toTokenAddress"); + String fromAddress = getIntent().getStringExtra("fromAddress"); + String fromAmount = getIntent().getStringExtra("fromAmount"); + long fromTokenDecimals = getIntent().getLongExtra("fromTokenDecimals", -1); + String slippage = getIntent().getStringExtra("slippage"); + String fromSymbol = getIntent().getStringExtra("fromTokenSymbol"); + String fromTokenLogoUri = getIntent().getStringExtra("fromTokenLogoUri"); + + this.fromAmount.setText(BalanceUtils.getShortFormat(fromAmount, fromTokenDecimals)); + this.fromSymbol.setText(fromSymbol); + this.fromTokenIcon.bindData(fromTokenLogoUri, Long.parseLong(fromChainId), fromTokenAddress, fromSymbol); + + viewModel.getRoutes(fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAddress, fromAmount, slippage, viewModel.getPreferredExchanges()); + } + + private void onRoutes(List routes) + { + processRoutes(routes); + + getRoutesTimer.start(); + } + + private void processRoutes(List routeList) + { + RouteAdapter adapter = new RouteAdapter(this, routeList, provider -> { + Intent intent = new Intent(); + intent.putExtra("provider", provider); + setResult(RESULT_OK, intent); + finish(); + }); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(adapter); + + if (!routeList.isEmpty()) + { + Route route = routeList.get(0); + currentPrice.setText(SwapUtils.getFormattedCurrentPrice(route.steps.get(0).action)); + noRoutesLayout.setVisibility(View.GONE); + } + else + { + currentPrice.setText(R.string.NA); + noRoutesLayout.setVisibility(View.VISIBLE); + } + } + + private void onProgressInfo(ProgressInfo progressInfo) + { + if (progressInfo.shouldShow()) + { + progressDialog.setMessage(progressInfo.getMessage()); + progressDialog.show(); + } + else + { + progressDialog.dismiss(); + } + } + + @Override + public void onBackPressed() + { + setResult(RESULT_CANCELED); + super.onBackPressed(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/SelectSwapProvidersActivity.java b/app/src/main/java/com/alphawallet/app/ui/SelectSwapProvidersActivity.java new file mode 100644 index 0000000000..d5822ebfbf --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/SelectSwapProvidersActivity.java @@ -0,0 +1,78 @@ +package com.alphawallet.app.ui; + +import android.os.Bundle; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.R; +import com.alphawallet.app.ui.widget.adapter.SwapProviderAdapter; +import com.alphawallet.app.viewmodel.SelectSwapProvidersViewModel; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class SelectSwapProvidersActivity extends BaseActivity +{ + private SelectSwapProvidersViewModel viewModel; + private SwapProviderAdapter adapter; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.basic_list_activity); + + toolbar(); + + setTitle(getString(R.string.title_select_exchanges)); + + initViewModel(); + + initViews(); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(SelectSwapProvidersViewModel.class); + } + + private void initViews() + { + RecyclerView recyclerView = findViewById(R.id.list); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + adapter = new SwapProviderAdapter(this, viewModel.getSwapProviders()); + recyclerView.setAdapter(adapter); + } + + @Override + public void onBackPressed() + { + if (viewModel.savePreferences(adapter.getExchanges())) + { + setResult(RESULT_OK); + super.onBackPressed(); + } + else + { + Toast.makeText(this, getString(R.string.message_select_one_exchange), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == android.R.id.home) + { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/SendActivity.java b/app/src/main/java/com/alphawallet/app/ui/SendActivity.java index d608f7ba76..b925de8cdb 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SendActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SendActivity.java @@ -22,6 +22,8 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.EIP681Type; import com.alphawallet.app.entity.NetworkInfo; @@ -35,7 +37,7 @@ import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.service.GasService; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; import com.alphawallet.app.ui.widget.entity.AddressReadyCallback; import com.alphawallet.app.ui.widget.entity.AmountReadyCallback; @@ -198,100 +200,96 @@ public void onBackPressed() @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - Operation taskCode = null; - if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) { - taskCode = Operation.values()[requestCode - SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS]; - requestCode = SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS; - } - - if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) - { - if (confirmationDialog != null && confirmationDialog.isShowing()) - confirmationDialog.completeSignRequest(resultCode == RESULT_OK); - } - else if (requestCode == C.BARCODE_READER_REQUEST_CODE) - { - switch (resultCode) + Operation taskCode = null; + if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) { - case Activity.RESULT_OK: - if (data != null) - { - String qrCode = data.getStringExtra(C.EXTRA_QR_CODE); + taskCode = Operation.values()[requestCode - SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS]; + requestCode = SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS; + } - //if barcode is still null, ensure we don't GPF - if (qrCode == null) - { - //Toast.makeText(this, R.string.toast_qr_code_no_address, Toast.LENGTH_SHORT).show(); - displayScanError(); - return; - } - else if (qrCode.startsWith("wc:")) - { - startWalletConnect(qrCode); - } - else + if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS && requestCode <= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + 10) + { + if (confirmationDialog != null && confirmationDialog.isShowing()) + confirmationDialog.completeSignRequest(resultCode == RESULT_OK); + } + else if (requestCode == C.BARCODE_READER_REQUEST_CODE) + { + switch (resultCode) + { + case Activity.RESULT_OK: + if (data != null) { + String qrCode = data.getStringExtra(C.EXTRA_QR_CODE); - QRParser parser = QRParser.getInstance(EthereumNetworkBase.extraChains()); - QRResult result = parser.parse(qrCode); - String extracted_address = null; - if (result != null) + //if barcode is still null, ensure we don't GPF + if (qrCode == null) { - extracted_address = result.getAddress(); - switch (result.getProtocol()) - { - case "address": - addressInput.setAddress(extracted_address); - break; - case "ethereum": - //EIP681 protocol - validateEIP681Request(result, false); - break; - default: - displayScanError(); - break; - } + //Toast.makeText(this, R.string.toast_qr_code_no_address, Toast.LENGTH_SHORT).show(); + displayScanError(); + return; } - else //try magiclink + else { - ParseMagicLink magicParser = new ParseMagicLink(new CryptoFunctions(), EthereumNetworkRepository.extraChains()); - try + QRParser parser = QRParser.getInstance(EthereumNetworkBase.extraChains()); + QRResult result = parser.parse(qrCode); + String extracted_address = null; + if (result != null) { - if (magicParser.parseUniversalLink(qrCode).chainId > 0) //see if it's a valid link + extracted_address = result.getAddress(); + switch (result.getProtocol()) { - //let's try to import the link - viewModel.showImportLink(this, qrCode); - finish(); - return; + case "address": + addressInput.setAddress(extracted_address); + break; + case "ethereum": + //EIP681 protocol + validateEIP681Request(result, false); + break; + default: + break; } } - catch (SalesOrderMalformed e) + else //try magiclink { - e.printStackTrace(); + ParseMagicLink magicParser = new ParseMagicLink(new CryptoFunctions(), EthereumNetworkRepository.extraChains()); + try + { + if (magicParser.parseUniversalLink(qrCode).chainId > 0) //see if it's a valid link + { + //let's try to import the link + viewModel.showImportLink(this, qrCode); + finish(); + return; + } + } + catch (SalesOrderMalformed e) + { + e.printStackTrace(); + } } - } - if (extracted_address == null) - { - displayScanError(); + if (extracted_address == null) + { + displayScanError(); + } } } - } - break; - case QRScanner.DENY_PERMISSION: - showCameraDenied(); - break; - default: - Timber.tag("SEND").e(String.format(getString(R.string.barcode_error_format), - "Code: " + resultCode - )); - break; + break; + case QRScannerActivity.DENY_PERMISSION: + showCameraDenied(); + break; + default: + Timber.tag("SEND").e(String.format(getString(R.string.barcode_error_format), + "Code: " + resultCode + )); + break; + } + } + else + { + super.onActivityResult(requestCode, resultCode, data); } - } - else - { - super.onActivityResult(requestCode, resultCode, data); } } @@ -481,8 +479,8 @@ protected void onDestroy() if (handler != null) handler.removeCallbacksAndMessages(null); if (amountInput != null) amountInput.onDestroy(); if (confirmationDialog != null) confirmationDialog.onDestroy(); - if (addressInput != null) - addressInput.setEnsNodeNotSyncCallback(null); // prevent leak by removing reference to activity method + //if (addressInput != null) + // addressInput.setEnsNodeNotSyncCallback(null); // prevent leak by removing reference to activity method } private void setupTokenContent() @@ -492,8 +490,8 @@ private void setupTokenContent() addressInput = findViewById(R.id.input_address); addressInput.setAddressCallback(this); addressInput.setChainOverrideForWalletConnect(token.tokenInfo.chainId); - addressInput.setEnsHandlerNodeSyncFlag(true); // allow node sync - addressInput.setEnsNodeNotSyncCallback(this::showNodeNotSyncSheet); // callback to invoke if node not synced + //addressInput.setEnsHandlerNodeSyncFlag(true); // allow node sync + //addressInput.setEnsNodeNotSyncCallback(this::showNodeNotSyncSheet); // callback to invoke if node not synced FunctionButtonBar functionBar = findViewById(R.id.layoutButtons); functionBar.revealButtons(); List functions = new ArrayList<>(Collections.singletonList(R.string.action_next)); @@ -655,7 +653,9 @@ public ActivityResultLauncher gasSelectLauncher() @Override public void notifyConfirm(String mode) { - viewModel.actionSheetConfirm(mode); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, props); } private void txWritten(TransactionData transactionData) @@ -712,7 +712,7 @@ void showNodeNotSyncSheet() alertDialog.setButtonListener(v -> alertDialog.dismiss()); alertDialog.setSecondaryButtonText(R.string.ignore); alertDialog.setSecondaryButtonListener(v -> { - addressInput.setEnsHandlerNodeSyncFlag(false); // skip node sync check + //addressInput.setEnsHandlerNodeSyncFlag(false); // skip node sync check // re enter current input to resolve again String currentInput = addressInput.getEditText().getText().toString(); addressInput.getEditText().setText(""); diff --git a/app/src/main/java/com/alphawallet/app/ui/SignDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/SignDetailActivity.java index d9906f1d85..4cc38733bb 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SignDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SignDetailActivity.java @@ -13,12 +13,10 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.repository.SignRecord; -import com.alphawallet.app.util.LocaleUtils; import com.alphawallet.app.util.Utils; +import com.alphawallet.app.widget.StandardHeader; -import java.text.DateFormat; import java.util.ArrayList; -import java.util.Date; import dagger.hilt.android.AndroidEntryPoint; @@ -41,7 +39,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) setTitle(getString(R.string.signed_transactions)); signRecords = getIntent().getParcelableArrayListExtra(C.EXTRA_STATE); - if (signRecords != null) { + if (signRecords != null) + { recyclerView = findViewById(R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new CustomAdapter(); @@ -60,28 +59,14 @@ public CustomAdapter.CustomViewHolder onCreateViewHolder(ViewGroup parent, int v return new CustomAdapter.CustomViewHolder(itemView); } - class CustomViewHolder extends RecyclerView.ViewHolder - { - TextView date; - TextView type; - TextView detail; - - CustomViewHolder(View view) - { - super(view); - date = view.findViewById(R.id.date); - type = view.findViewById(R.id.sign_type); - detail = view.findViewById(R.id.details); - } - } - @Override public void onBindViewHolder(CustomAdapter.CustomViewHolder holder, int position) { SignRecord record = signRecords.get(position); - - holder.date.setText(Utils.localiseUnixTime(getApplicationContext(), record.date/1000)); - holder.type.setText(record.type); + holder.date.setText(record.type + + " (" + + Utils.localiseUnixTime(getApplicationContext(), record.date / 1000) + + ")"); holder.detail.setText(record.message); } @@ -90,5 +75,18 @@ public int getItemCount() { return signRecords.size(); } + + class CustomViewHolder extends RecyclerView.ViewHolder + { + StandardHeader date; + TextView detail; + + CustomViewHolder(View view) + { + super(view); + date = view.findViewById(R.id.date); + detail = view.findViewById(R.id.details); + } + } } } diff --git a/app/src/main/java/com/alphawallet/app/ui/SplashActivity.java b/app/src/main/java/com/alphawallet/app/ui/SplashActivity.java index 2b57e83816..90c1cf0046 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SplashActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SplashActivity.java @@ -13,10 +13,13 @@ import androidx.lifecycle.ViewModelProvider; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CreateWalletCallbackInterface; import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.analytics.FirstWalletAction; import com.alphawallet.app.router.HomeRouter; import com.alphawallet.app.router.ImportWalletRouter; import com.alphawallet.app.service.KeyService; @@ -25,14 +28,12 @@ import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.SignTransactionDialog; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SplashActivity extends BaseActivity implements CreateWalletCallbackInterface, Runnable { - SplashViewModel splashViewModel; + private SplashViewModel viewModel; private Handler handler = new Handler(Looper.getMainLooper()); private String errorMessage; @@ -48,15 +49,15 @@ protected void onCreate(Bundle savedInstanceState) super.onCreate(savedInstanceState); //detect previous launch - splashViewModel = new ViewModelProvider(this) + viewModel = new ViewModelProvider(this) .get(SplashViewModel.class); - splashViewModel.cleanAuxData(getApplicationContext()); + viewModel.cleanAuxData(getApplicationContext()); setContentView(R.layout.activity_splash); - splashViewModel.wallets().observe(this, this::onWallets); - splashViewModel.createWallet().observe(this, this::onWalletCreate); - splashViewModel.fetchWallets(); + viewModel.wallets().observe(this, this::onWallets); + viewModel.createWallet().observe(this, this::onWalletCreate); + viewModel.fetchWallets(); checkRoot(); } @@ -85,15 +86,21 @@ private void onWallets(Wallet[] wallets) { // - no - proceed to home activity if (wallets.length == 0) { - splashViewModel.setDefaultBrowser(); + viewModel.setDefaultBrowser(); findViewById(R.id.layout_new_wallet).setVisibility(View.VISIBLE); findViewById(R.id.button_create).setOnClickListener(v -> { - splashViewModel.createNewWallet(this, this); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(FirstWalletAction.KEY, FirstWalletAction.CREATE_WALLET.getValue()); + viewModel.track(Analytics.Action.FIRST_WALLET_ACTION, props); + viewModel.createNewWallet(this, this); }); findViewById(R.id.button_watch).setOnClickListener(v -> { new ImportWalletRouter().openWatchCreate(this, IMPORT_REQUEST_CODE); }); findViewById(R.id.button_import).setOnClickListener(v -> { + AnalyticsProperties props = new AnalyticsProperties(); + props.put(FirstWalletAction.KEY, FirstWalletAction.IMPORT_WALLET.getValue()); + viewModel.track(Analytics.Action.FIRST_WALLET_ACTION, props); new ImportWalletRouter().openForResult(this, IMPORT_REQUEST_CODE); }); } @@ -112,23 +119,23 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Operation taskCode = Operation.values()[requestCode - SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS]; if (resultCode == RESULT_OK) { - splashViewModel.completeAuthentication(taskCode); + viewModel.completeAuthentication(taskCode); } else { - splashViewModel.failedAuthentication(taskCode); + viewModel.failedAuthentication(taskCode); } } else if (requestCode == IMPORT_REQUEST_CODE) { - splashViewModel.fetchWallets(); + viewModel.fetchWallets(); } } @Override public void HDKeyCreated(String address, Context ctx, KeyService.AuthenticationLevel level) { - splashViewModel.StoreHDKey(address, level); + viewModel.StoreHDKey(address, level); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/SupportSettingsActivity.java b/app/src/main/java/com/alphawallet/app/ui/SupportSettingsActivity.java index 1947e000a3..d472c6c1ca 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SupportSettingsActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SupportSettingsActivity.java @@ -7,22 +7,23 @@ import android.widget.LinearLayout; import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.MediaLinks; +import com.alphawallet.app.viewmodel.SupportSettingsViewModel; import com.alphawallet.app.widget.SettingsItemView; -import timber.log.Timber; - import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; @AndroidEntryPoint -public class SupportSettingsActivity extends BaseActivity { - +public class SupportSettingsActivity extends BaseActivity +{ + private SupportSettingsViewModel viewModel; private LinearLayout supportSettingsLayout; - private SettingsItemView telegram; private SettingsItemView discord; private SettingsItemView email; @@ -34,19 +35,36 @@ public class SupportSettingsActivity extends BaseActivity { private SettingsItemView github; @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) + { super.onCreate(savedInstanceState); setContentView(R.layout.activity_generic_settings); toolbar(); + setTitle(getString(R.string.title_support)); + initViewModel(); + initializeSettings(); addSettingsToLayout(); } - private void initializeSettings() { + @Override + protected void onResume() + { + super.onResume(); + viewModel.track(Analytics.Navigation.SETTINGS_SUPPORT); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this).get(SupportSettingsViewModel.class); + } + + private void initializeSettings() + { telegram = new SettingsItemView.Builder(this) .withIcon(R.drawable.ic_logo_telegram) .withTitle(R.string.telegram) @@ -102,25 +120,31 @@ private void initializeSettings() { .build(); } - private void addSettingsToLayout() { + private void addSettingsToLayout() + { supportSettingsLayout = findViewById(R.id.layout); - if (MediaLinks.AWALLET_TELEGRAM_URL != null) { + if (MediaLinks.AWALLET_TELEGRAM_URL != null) + { supportSettingsLayout.addView(telegram); } - if (MediaLinks.AWALLET_DISCORD_URL != null){ + if (MediaLinks.AWALLET_DISCORD_URL != null) + { supportSettingsLayout.addView(discord); } - if (MediaLinks.AWALLET_EMAIL1 != null) { + if (MediaLinks.AWALLET_EMAIL1 != null) + { supportSettingsLayout.addView(email); } - if (MediaLinks.AWALLET_TWITTER_URL != null) { + if (MediaLinks.AWALLET_TWITTER_URL != null) + { supportSettingsLayout.addView(twitter); } - if (MediaLinks.AWALLET_GITHUB != null) { + if (MediaLinks.AWALLET_GITHUB != null) + { supportSettingsLayout.addView(github); } @@ -138,47 +162,67 @@ private void addSettingsToLayout() { supportSettingsLayout.addView(faq); } - private void onTelegramClicked() { + private void onTelegramClicked() + { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(MediaLinks.AWALLET_TELEGRAM_URL)); - if (isAppAvailable(C.TELEGRAM_PACKAGE_NAME)) { + if (isAppAvailable(C.TELEGRAM_PACKAGE_NAME)) + { intent.setPackage(C.TELEGRAM_PACKAGE_NAME); } - try { + try + { + viewModel.track(Analytics.Action.SUPPORT_TELEGRAM); startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onGitHubClicked() { + private void onGitHubClicked() + { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(MediaLinks.AWALLET_GITHUB)); - try { + try + { + viewModel.track(Analytics.Action.SUPPORT_GITHUB); startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onDiscordClicked(){ + private void onDiscordClicked() + { Intent intent; - try { + try + { intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_DISCORD_URL)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } catch (Exception e) { + } + catch (Exception e) + { intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_DISCORD_URL)); } - try { + try + { + viewModel.track(Analytics.Action.SUPPORT_DISCORD); startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onEmailClicked() { + private void onEmailClicked() + { Intent intent = new Intent(Intent.ACTION_SENDTO); final String at = "@"; String email = @@ -187,94 +231,133 @@ private void onEmailClicked() { "&body=" + Uri.encode(""); intent.setData(Uri.parse(email)); - try { + try + { + viewModel.track(Analytics.Action.SUPPORT_EMAIL); startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onLinkedInClicked() { + private void onLinkedInClicked() + { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(MediaLinks.AWALLET_LINKEDIN_URL)); - if (isAppAvailable(C.LINKEDIN_PACKAGE_NAME)) { + if (isAppAvailable(C.LINKEDIN_PACKAGE_NAME)) + { intent.setPackage(C.LINKEDIN_PACKAGE_NAME); } - try { + try + { startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onTwitterClicked() { + private void onTwitterClicked() + { Intent intent; - try { + try + { getPackageManager().getPackageInfo(C.TWITTER_PACKAGE_NAME, 0); intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_TWITTER_URL)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } catch (Exception e) { + } + catch (Exception e) + { intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_TWITTER_URL)); } - try { + try + { + viewModel.track(Analytics.Action.SUPPORT_TWITTER); startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onRedditClicked() { + private void onRedditClicked() + { Intent intent = new Intent(Intent.ACTION_VIEW); - if (isAppAvailable(C.REDDIT_PACKAGE_NAME)) { + if (isAppAvailable(C.REDDIT_PACKAGE_NAME)) + { intent.setPackage(C.REDDIT_PACKAGE_NAME); } intent.setData(Uri.parse(MediaLinks.AWALLET_REDDIT_URL)); - try { + try + { startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onFacebookClicked() { + private void onFacebookClicked() + { Intent intent; - try { + try + { getPackageManager().getPackageInfo(C.FACEBOOK_PACKAGE_NAME, 0); intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_FACEBOOK_URL)); //intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_FACEBOOK_ID)); - } catch (Exception e) { + } + catch (Exception e) + { intent = new Intent(Intent.ACTION_VIEW, Uri.parse(MediaLinks.AWALLET_FACEBOOK_URL)); } - try { + try + { startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private void onBlogClicked() { + private void onBlogClicked() + { } - private void onFaqClicked() { + private void onFaqClicked() + { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(MediaLinks.AWALLET_FAQ_URL)); - try { + try + { + viewModel.track(Analytics.Action.SUPPORT_FAQ); startActivity(intent); - } catch (Exception e) { + } + catch (Exception e) + { Timber.e(e); } } - private boolean isAppAvailable(String packageName) { + private boolean isAppAvailable(String packageName) + { PackageManager pm = getPackageManager(); - try { + try + { pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); return true; - } catch (Exception e) { + } + catch (Exception e) + { return false; } } diff --git a/app/src/main/java/com/alphawallet/app/ui/SwapActivity.java b/app/src/main/java/com/alphawallet/app/ui/SwapActivity.java index 3ae7faa618..91775526af 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SwapActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SwapActivity.java @@ -2,6 +2,7 @@ import android.content.Intent; import android.os.Bundle; +import android.os.CountDownTimer; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; @@ -11,27 +12,36 @@ import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TransactionData; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.analytics.ActionSheetSource; import com.alphawallet.app.entity.lifi.Chain; import com.alphawallet.app.entity.lifi.Connection; import com.alphawallet.app.entity.lifi.Quote; -import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.entity.lifi.Token; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; +import com.alphawallet.app.ui.widget.entity.ProgressInfo; import com.alphawallet.app.util.BalanceUtils; +import com.alphawallet.app.util.SwapUtils; import com.alphawallet.app.viewmodel.SwapViewModel; +import com.alphawallet.app.viewmodel.Tokens; import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.ActionSheetDialog; import com.alphawallet.app.widget.SelectTokenDialog; +import com.alphawallet.app.widget.StandardHeader; import com.alphawallet.app.widget.SwapSettingsDialog; import com.alphawallet.app.widget.TokenInfoView; import com.alphawallet.app.widget.TokenSelector; @@ -39,8 +49,6 @@ import com.alphawallet.token.tools.Numeric; import com.google.android.material.button.MaterialButton; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -49,35 +57,40 @@ @AndroidEntryPoint public class SwapActivity extends BaseActivity implements StandardFunctionInterface, ActionSheetCallback { - SwapViewModel viewModel; - - private TextView chainName; - + private static final long GET_QUOTE_INTERVAL_MS = 30000; + private static final long COUNTDOWN_INTERVAL_MS = 1000; + private SwapViewModel viewModel; private TokenSelector sourceSelector; private TokenSelector destSelector; - private SelectTokenDialog sourceTokenDialog; private SelectTokenDialog destTokenDialog; - - //private ConfirmSwapDialog confirmSwapDialog; private ActionSheetDialog confirmationDialog; private SwapSettingsDialog settingsDialog; private AWalletAlertDialog progressDialog; - + private AWalletAlertDialog errorDialog; private RelativeLayout tokenLayout; private LinearLayout infoLayout; - private TokenInfoView fees; + private StandardHeader quoteHeader; + private TokenInfoView provider; + private TokenInfoView providerWebsite; + private TokenInfoView gasFees; + private TokenInfoView otherFees; private TokenInfoView currentPrice; private TokenInfoView minReceived; private LinearLayout noConnectionsLayout; private MaterialButton continueBtn; private MaterialButton openSettingsBtn; - - private Token token; + private TextView chainName; + private com.alphawallet.app.entity.tokens.Token token; private Wallet wallet; - private Connection.LToken sourceToken; - + private Token sourceToken; private List chains; + private String selectedRouteProvider; + private CountDownTimer getQuoteTimer; + private ActivityResultLauncher selectSwapProviderLauncher; + private ActivityResultLauncher gasSettingsLauncher; + private ActivityResultLauncher getRoutesLauncher; + private AnalyticsProperties confirmationDialogProps; @Override protected void onCreate(@Nullable Bundle savedInstanceState) @@ -88,13 +101,50 @@ protected void onCreate(@Nullable Bundle savedInstanceState) toolbar(); - setTitle("Swap"); + setTitle(getString(R.string.swap)); initViewModel(); getIntentData(); initViews(); + + initTimer(); + + registerActivityResultLaunchers(); + + viewModel.prepare(this, selectSwapProviderLauncher); + } + + private void registerActivityResultLaunchers() + { + selectSwapProviderLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) + { + viewModel.getChains(); + } + }); + + gasSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> confirmationDialog.setCurrentGasIndex(result)); + + getRoutesLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) + { + Intent data = result.getData(); + if (data != null) + { + selectedRouteProvider = data.getStringExtra("provider"); + getQuote(); + } + } + else if (result.getResultCode() == RESULT_CANCELED) + { + continueBtn.setEnabled(!TextUtils.isEmpty(selectedRouteProvider)); + } + }); } private void initViewModel() @@ -112,6 +162,24 @@ private void initViewModel() viewModel.transactionError().observe(this, this::txError); } + private void initTimer() + { + getQuoteTimer = new CountDownTimer(GET_QUOTE_INTERVAL_MS, COUNTDOWN_INTERVAL_MS) + { + @Override + public void onTick(long millisUntilFinished) + { + // TODO: Display countdown timer? + } + + @Override + public void onFinish() + { + getQuote(); + } + }; + } + private void getIntentData() { long chainId = getIntent().getLongExtra(C.EXTRA_CHAIN_ID, EthereumNetworkBase.MAINNET_ID); @@ -126,13 +194,21 @@ private void initViews() destSelector = findViewById(R.id.to_input); tokenLayout = findViewById(R.id.layout_tokens); infoLayout = findViewById(R.id.layout_info); - fees = findViewById(R.id.tiv_fees); + quoteHeader = findViewById(R.id.header_quote); + provider = findViewById(R.id.tiv_provider); + providerWebsite = findViewById(R.id.tiv_provider_website); + gasFees = findViewById(R.id.tiv_gas_fees); + otherFees = findViewById(R.id.tiv_other_fees); currentPrice = findViewById(R.id.tiv_current_price); minReceived = findViewById(R.id.tiv_min_received); noConnectionsLayout = findViewById(R.id.layout_no_connections); continueBtn = findViewById(R.id.btn_continue); openSettingsBtn = findViewById(R.id.btn_open_settings); + quoteHeader.getImageControl().setOnClickListener(v -> { + getAvailableRoutes(); + }); + progressDialog = new AWalletAlertDialog(this); progressDialog.setCancelable(false); progressDialog.setProgressMode(); @@ -166,11 +242,18 @@ public void onSelectorClicked() @Override public void onAmountChanged(String amount) { - getQuote(); + if (TextUtils.isEmpty(selectedRouteProvider)) + { + getAvailableRoutes(); + } + else + { + getQuote(); + } } @Override - public void onSelectionChanged(Connection.LToken token) + public void onSelectionChanged(Token token) { sourceTokenChanged(token); } @@ -178,7 +261,13 @@ public void onSelectionChanged(Connection.LToken token) @Override public void onMaxClicked() { - String max = viewModel.getBalance(wallet.address, sourceSelector.getToken()); + Token token = sourceSelector.getToken(); + if (token == null) + { + return; + } + + String max = viewModel.getBalance(token); if (!max.isEmpty()) { sourceSelector.setAmount(max); @@ -201,7 +290,7 @@ public void onAmountChanged(String amount) } @Override - public void onSelectionChanged(Connection.LToken token) + public void onSelectionChanged(Token token) { destTokenChanged(token); } @@ -220,6 +309,7 @@ private void showConfirmDialog() { confirmationDialog.show(); confirmationDialog.fullExpand(); + viewModel.track(Analytics.Navigation.ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION, confirmationDialogProps); } } @@ -228,13 +318,18 @@ private ActionSheetDialog createConfirmationAction(Quote quote) ActionSheetDialog confDialog = null; try { - Token activeToken = viewModel.getTokensService().getTokenOrBase(sourceToken.chainId, sourceToken.address); + com.alphawallet.app.entity.tokens.Token activeToken = viewModel.getTokensService().getTokenOrBase(sourceToken.chainId, sourceToken.address); Web3Transaction w3Tx = viewModel.buildWeb3Transaction(quote); confDialog = new ActionSheetDialog(this, w3Tx, activeToken, "", w3Tx.recipient.toString(), viewModel.getTokensService(), this); - confDialog.setURL("LI.FI Best Quote"); //TODO: Expand swap provider here + confDialog.setURL(quote.swapProvider.name); confDialog.setCanceledOnTouchOutside(false); confDialog.setGasEstimate(Numeric.toBigInt(quote.transactionRequest.gasLimit)); + + confirmationDialogProps = new AnalyticsProperties(); + confirmationDialogProps.put(Analytics.PROPS_ACTION_SHEET_SOURCE, ActionSheetSource.SWAP.getValue()); + confirmationDialogProps.put(Analytics.PROPS_SWAP_FROM_TOKEN, quote.action.fromToken.symbol); + confirmationDialogProps.put(Analytics.PROPS_SWAP_TO_TOKEN, quote.action.toToken.symbol); } catch (Exception e) { @@ -244,25 +339,31 @@ private ActionSheetDialog createConfirmationAction(Quote quote) return confDialog; } - private void destTokenChanged(Connection.LToken token) + private void destTokenChanged(Token token) { - destSelector.setBalance(viewModel.getBalance(wallet.address, token)); + destSelector.setBalance(viewModel.getBalance(token)); infoLayout.setVisibility(View.GONE); destTokenDialog.setSelectedToken(token.address); - getQuote(); + selectedRouteProvider = ""; + + getAvailableRoutes(); } - private void sourceTokenChanged(Connection.LToken token) + private void sourceTokenChanged(Token token) { if (destSelector.getToken() == null) { destSelector.setVisibility(View.VISIBLE); } - sourceSelector.setBalance(viewModel.getBalance(wallet.address, token)); + sourceSelector.clearAmount(); + + destSelector.clearAmount(); + + sourceSelector.setBalance(viewModel.getBalance(token)); infoLayout.setVisibility(View.GONE); @@ -270,18 +371,35 @@ private void sourceTokenChanged(Connection.LToken token) sourceToken = token; - getQuote(); + selectedRouteProvider = ""; + + getAvailableRoutes(); } @Override protected void onResume() { super.onResume(); - viewModel.getChains(); + viewModel.track(Analytics.Navigation.TOKEN_SWAP); + + if (settingsDialog != null) + { + settingsDialog.setSwapProviders(viewModel.getPreferredSwapProviders()); + } + } + + @Override + protected void onPause() + { + if (getQuoteTimer != null) + { + getQuoteTimer.cancel(); + } + super.onPause(); } // The source token should default to the token selected in the main wallet dialog (ie the token from the intent). - private void initSourceToken(Connection.LToken selectedToken) + private void initSourceToken(Token selectedToken) { if (selectedToken != null) { @@ -293,65 +411,48 @@ private void initSourceToken(Connection.LToken selectedToken) sourceSelector.reset(); infoLayout.setVisibility(View.GONE); } - - //TODO: Add base 'ETH' to dest tokens in selector - /*long networkId = fromTokens.get(0).chainId; - - String symbol = "eth"; - - for (Chain c : chains) - { - if (c.id == networkId) - { - symbol = c.coin; - } - } - - boolean matchFound = false; - - for (Connection.LToken t : fromTokens) - { - if (t.symbol.equalsIgnoreCase(symbol)) - { - sourceSelector.init(t); - matchFound = true; - break; - } - } - - if (!matchFound) - { - sourceSelector.reset(); - - infoLayout.setVisibility(View.GONE); - }*/ } - private void initFromDialog(List fromTokens) + private void initFromDialog(List fromTokens) { + Tokens.sortValue(fromTokens); sourceTokenDialog = new SelectTokenDialog(fromTokens, this, tokenItem -> { sourceSelector.init(tokenItem); sourceTokenDialog.dismiss(); }); } - private void initToDialog(List toTokens) + private void initToDialog(List toTokens) { + Tokens.sortName(toTokens); + Tokens.sortValue(toTokens); destTokenDialog = new SelectTokenDialog(toTokens, this, tokenItem -> { destSelector.init(tokenItem); destTokenDialog.dismiss(); }); } - private void getQuote() + private void getAvailableRoutes() { - continueBtn.setEnabled(false); + if (getQuoteTimer != null) + { + getQuoteTimer.cancel(); + } if (sourceSelector.getToken() != null && destSelector.getToken() != null + && !sourceSelector.getToken().equals(destSelector.getToken()) && !TextUtils.isEmpty(sourceSelector.getAmount())) { - viewModel.getQuote(sourceSelector.getToken(), destSelector.getToken(), wallet.address, sourceSelector.getAmount(), settingsDialog.getSlippage()); + viewModel.getRoutes( + this, + getRoutesLauncher, + sourceSelector.getToken(), + destSelector.getToken(), + wallet.address, + sourceSelector.getAmount(), + settingsDialog.getSlippage() + ); } } @@ -359,7 +460,11 @@ private void onChains(List chains) { this.chains = chains; - settingsDialog = new SwapSettingsDialog(this, chains, + settingsDialog = new SwapSettingsDialog( + this, + chains, + viewModel.getSwapProviders(), + viewModel.getPreferredSwapProviders(), chain -> { chainName.setText(chain.name); viewModel.setChain(chain); @@ -399,31 +504,37 @@ private void onConnections(List connections) { if (!connections.isEmpty()) { - List fromTokens = new ArrayList<>(); - List toTokens = new ArrayList<>(); - Connection.LToken selectedToken = null; + List fromTokens = new ArrayList<>(); + List toTokens = new ArrayList<>(); + Token selectedToken = null; for (Connection c : connections) { - for (Connection.LToken t : c.fromTokens) + for (Token t : c.fromTokens) { if (!fromTokens.contains(t)) { - t.balance = viewModel.getBalance(wallet.address, t); - fromTokens.add(t); + t.balance = viewModel.getBalance(t); + t.fiatEquivalent = t.getFiatValue(); - if (t.chainId == token.tokenInfo.chainId && t.address.equalsIgnoreCase(token.getAddress())) + if (t.fiatEquivalent > 0) { - selectedToken = t; + fromTokens.add(t); + + if (t.isSimilarTo(token, wallet.address)) + { + selectedToken = t; + } } } } - for (Connection.LToken t : c.toTokens) + for (Token t : c.toTokens) { if (!toTokens.contains(t)) { - t.balance = viewModel.getBalance(wallet.address, t); + t.balance = viewModel.getBalance(t); + t.fiatEquivalent = t.getFiatValue(); toTokens.add(t); } } @@ -444,7 +555,26 @@ private void onConnections(List connections) infoLayout.setVisibility(View.GONE); noConnectionsLayout.setVisibility(View.VISIBLE); } + } + private void getQuote() + { + if (!TextUtils.isEmpty(selectedRouteProvider)) + { + if (errorDialog != null && errorDialog.isShowing()) + { + errorDialog.dismiss(); + } + + viewModel.getQuote( + sourceSelector.getToken(), + destSelector.getToken(), + wallet.address, + sourceSelector.getAmount(), + settingsDialog.getSlippage(), + selectedRouteProvider + ); + } } private void onQuote(Quote quote) @@ -459,6 +589,8 @@ private void onQuote(Quote quote) } continueBtn.setEnabled(true); + + getQuoteTimer.start(); } private void updateDestAmount(Quote quote) @@ -475,48 +607,31 @@ private void updateDestAmount(Quote quote) private void updateInfoSummary(Quote quote) { - //convert gasQuote to Eth cost - BigInteger gasCost = Numeric.toBigInt(quote.transactionRequest.gasPrice); - BigInteger gasLimit = Numeric.toBigInt(quote.transactionRequest.gasLimit); - - BigInteger networkFee = gasCost.multiply(gasLimit); - - String ethCostStr = BalanceUtils.getScaledValueFixed(new BigDecimal(networkFee), 18, 4); - - fees.setValue(ethCostStr); //TODO: Needs to say 'Eth' after the quote, also should get the Eth price to show the Tx cost in user's Fiat - //TODO: To see this done check GasWidget, see comment "Can we display value for gas?" - - BigDecimal s = new BigDecimal(quote.action.fromToken.priceUSD); - BigDecimal d = new BigDecimal(quote.action.toToken.priceUSD); - BigDecimal c = s.multiply(d); - String currentPriceTxt = "1 " + quote.action.fromToken.symbol + " ≈ " + c.toString() + " " + quote.action.toToken.symbol; - currentPrice.setValue(currentPriceTxt.trim()); - - String minReceivedVal = BalanceUtils.getShortFormat(quote.estimate.toAmountMin, quote.action.toToken.decimals) + " " + quote.action.toToken.symbol; - minReceived.setValue(minReceivedVal.trim()); - + provider.setValue(quote.swapProvider.name); + String url = viewModel.getSwapProviderUrl(quote.swapProvider.key); + if (!TextUtils.isEmpty(url)) + { + providerWebsite.setValue(url); + providerWebsite.setLink(); + } + gasFees.setValue(SwapUtils.getTotalGasFees(quote.estimate.gasCosts)); + otherFees.setValue(SwapUtils.getOtherFees(quote.estimate.feeCosts)); + currentPrice.setValue(SwapUtils.getFormattedCurrentPrice(quote.action).trim()); + minReceived.setValue(SwapUtils.getFormattedMinAmount(quote.estimate, quote.action)); infoLayout.setVisibility(View.VISIBLE); } - private void onProgressInfo(int code) + private void onProgressInfo(ProgressInfo progressInfo) { - String message; - switch (code) + if (progressInfo.shouldShow()) { - case C.ProgressInfo.FETCHING_CHAINS: - message = getString(R.string.message_fetching_chains); - break; - case C.ProgressInfo.FETCHING_CONNECTIONS: - message = getString(R.string.message_fetching_connections); - break; - case C.ProgressInfo.FETCHING_QUOTE: - message = getString(R.string.message_fetching_quote); - break; - default: - message = getString(R.string.title_dialog_handling); - break; + progressDialog.setTitle(progressInfo.getMessage()); + progressDialog.show(); + } + else + { + progressDialog.dismiss(); } - progressDialog.setTitle(message); } private void onProgress(Boolean shouldShowProgress) @@ -537,6 +652,8 @@ private void txWritten(TransactionData transactionData) successDialog.setTitle(R.string.transaction_succeeded); successDialog.setMessage(transactionData.txHash); successDialog.show(); + + viewModel.track(Analytics.Navigation.ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION_SUCCESSFUL, confirmationDialogProps); } private void txError(Throwable throwable) @@ -545,6 +662,9 @@ private void txError(Throwable throwable) errorDialog.setTitle(R.string.error_transaction_failed); errorDialog.setMessage(throwable.getMessage()); errorDialog.show(); + + confirmationDialogProps.put(Analytics.PROPS_ERROR_MESSAGE, throwable.getMessage()); + viewModel.track(Analytics.Navigation.ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION_FAILED, confirmationDialogProps); } private void onError(ErrorEnvelope errorEnvelope) @@ -554,12 +674,41 @@ private void onError(ErrorEnvelope errorEnvelope) case C.ErrorCode.INSUFFICIENT_BALANCE: sourceSelector.setError(getString(R.string.error_insufficient_balance, sourceSelector.getToken().symbol)); break; + case C.ErrorCode.SWAP_TIMEOUT_ERROR: + getAvailableRoutes(); + break; + case C.ErrorCode.SWAP_CONNECTIONS_ERROR: + case C.ErrorCode.SWAP_CHAIN_ERROR: + errorDialog = new AWalletAlertDialog(this); + errorDialog.setTitle(R.string.title_dialog_error); + errorDialog.setMessage(errorEnvelope.message); + errorDialog.setButton(R.string.try_again, v -> { + viewModel.getChains(); + errorDialog.dismiss(); + }); + errorDialog.setSecondaryButton(R.string.action_cancel, v -> errorDialog.dismiss()); + errorDialog.show(); + viewModel.trackError(Analytics.Error.TOKEN_SWAP, errorEnvelope.message); + break; + case C.ErrorCode.SWAP_QUOTE_ERROR: + errorDialog = new AWalletAlertDialog(this); + errorDialog.setTitle(R.string.title_dialog_error); + errorDialog.setMessage(errorEnvelope.message); + errorDialog.setButton(R.string.try_again, v -> { + getAvailableRoutes(); + errorDialog.dismiss(); + }); + errorDialog.setSecondaryButton(R.string.action_cancel, v -> errorDialog.dismiss()); + errorDialog.show(); + viewModel.trackError(Analytics.Error.TOKEN_SWAP, errorEnvelope.message); + break; default: - AWalletAlertDialog errorDialog = new AWalletAlertDialog(this); + errorDialog = new AWalletAlertDialog(this); errorDialog.setTitle(R.string.title_dialog_error); errorDialog.setMessage(errorEnvelope.message); errorDialog.setButton(R.string.action_cancel, v -> errorDialog.dismiss()); errorDialog.show(); + viewModel.trackError(Analytics.Error.TOKEN_SWAP, errorEnvelope.message); break; } } @@ -587,7 +736,19 @@ public boolean onOptionsItemSelected(MenuItem item) @Override public void getAuthorisation(SignAuthenticationCallback callback) { - viewModel.getAuthentication(this, wallet, callback); + if (wallet.type != WalletType.WATCH) + { + viewModel.getAuthentication(this, wallet, callback); + } + else + { + confirmationDialog.dismiss(); + errorDialog = new AWalletAlertDialog(this); + errorDialog.setTitle(R.string.title_dialog_error); + errorDialog.setMessage(getString(R.string.error_message_watch_only_wallet)); + errorDialog.setButton(R.string.dialog_ok, v -> errorDialog.dismiss()); + errorDialog.show(); + } } @Override @@ -599,18 +760,22 @@ public void sendTransaction(Web3Transaction tx) @Override public void dismissed(String txHash, long callbackId, boolean actionCompleted) { - + if (!actionCompleted && TextUtils.isEmpty(txHash)) + { + viewModel.track(Analytics.Action.ACTION_SHEET_CANCELLED, confirmationDialogProps); + } } @Override public void notifyConfirm(String mode) { - + confirmationDialogProps.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, confirmationDialogProps); } @Override public ActivityResultLauncher gasSelectLauncher() { - return null; + return gasSettingsLauncher; } } diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java index 2824943515..2cb601a25a 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java @@ -56,7 +56,7 @@ import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.ActionSheetDialog; -import com.alphawallet.app.widget.ActionSheetMode; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.app.widget.AmountDisplayWidget; import com.alphawallet.app.widget.ChainName; import com.alphawallet.app.widget.EventDetailWidget; @@ -552,7 +552,7 @@ private void populateActivityInfo(RealmAuxData item, String transactionValue) tokenView.setChainId(token.tokenInfo.chainId); tokenView.setWalletAddress(new Address(token.getWallet())); - tokenView.setRpcUrl(token.tokenInfo.chainId); + tokenView.setRpcUrl(viewModel.getBrowserRPC(token.tokenInfo.chainId)); tokenView.setOnReadyCallback(this); tokenView.setOnSetValuesListener(this); tokenView.setKeyboardListenerCallback(this); @@ -650,7 +650,7 @@ private void onAttr(TokenScriptResult.Attribute attribute) { //is the attr incomplete? Timber.d("ATTR/FA: " + attribute.id + " (" + attribute.name + ")" + " : " + attribute.text); - TokenScriptResult.addPair(attrs, attribute.id, attribute.text); + TokenScriptResult.addPair(attrs, attribute.id, attribute.text); } private void displayFunction(String tokenAttrs) diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java index d6e1084c74..fbd49b5b89 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java @@ -1,5 +1,11 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.C.EXTRA_STATE; +import static com.alphawallet.app.C.EXTRA_TOKENID_LIST; +import static com.alphawallet.app.C.Key.WALLET; +import static com.alphawallet.app.entity.DisplayState.TRANSFER_TO_ADDRESS; +import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; + import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -39,15 +45,6 @@ import java.util.List; import java.util.Map; -import javax.inject.Inject; - - -import static com.alphawallet.app.C.EXTRA_STATE; -import static com.alphawallet.app.C.EXTRA_TOKENID_LIST; -import static com.alphawallet.app.C.Key.WALLET; -import static com.alphawallet.app.entity.DisplayState.TRANSFER_TO_ADDRESS; -import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; - import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java index 850988684e..f1f402814b 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java @@ -20,10 +20,13 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TransactionData; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.analytics.ActionSheetSource; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.entity.RealmToken; import com.alphawallet.app.service.GasService; @@ -41,6 +44,7 @@ import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.token.entity.TSAction; import com.alphawallet.token.entity.TicketRange; +import com.alphawallet.token.entity.ViewType; import java.math.BigInteger; import java.util.HashMap; @@ -74,6 +78,7 @@ public class TokenFunctionActivity extends BaseActivity implements StandardFunct private Realm realm = null; private RealmResults realmTokenUpdates; private ActionSheetDialog confirmationDialog; + private AnalyticsProperties confirmationDialogProps; private void initViews(Token t) { token = t; @@ -85,7 +90,7 @@ private void initViews(Token t) { TicketRange data = new TicketRange(idList, token.tokenInfo.address, false); - tokenView.displayTicketHolder(token, data, viewModel.getAssetDefinitionService(), false); + tokenView.displayTicketHolder(token, data, viewModel.getAssetDefinitionService(), ViewType.VIEW); tokenView.setOnReadyCallback(this); tokenView.setOnSetValuesListener(this); @@ -121,6 +126,8 @@ private void txError(Throwable throwable) { throwable.getStackTrace(); Timber.d("ERROR: %s", throwable.getMessage()); + + viewModel.trackError(Analytics.Error.TOKEN_SCRIPT, throwable.getMessage()); } private void onWalletUpdate(Wallet w) @@ -300,6 +307,10 @@ private void checkConfirm(Web3Transaction w3tx) confirmationDialog.setURL("TokenScript"); confirmationDialog.setCanceledOnTouchOutside(false); confirmationDialog.show(); + + confirmationDialogProps = new AnalyticsProperties(); + confirmationDialogProps.put(Analytics.PROPS_ACTION_SHEET_SOURCE, ActionSheetSource.TOKENSCRIPT.getValue()); + viewModel.track(Analytics.Navigation.ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION, confirmationDialogProps); } } @@ -310,6 +321,7 @@ private void checkConfirm(Web3Transaction w3tx) private void txWritten(TransactionData transactionData) { confirmationDialog.transactionWritten(transactionData.txHash); //display hash and success in ActionSheet, start 1 second timer to dismiss. + viewModel.track(Analytics.Navigation.ACTION_SHEET_FOR_TRANSACTION_CONFIRMATION_SUCCESSFUL, confirmationDialogProps); } private void calculateEstimateDialog() @@ -335,6 +347,8 @@ private void errorInsufficientFunds(Token currency) dialog.setButtonText(R.string.button_ok); dialog.setButtonListener(v -> dialog.dismiss()); dialog.show(); + + viewModel.trackError(Analytics.Error.TOKEN_SCRIPT, getString(R.string.error_insufficient_funds)); } private void estimateError(final Web3Transaction w3tx) @@ -390,7 +404,7 @@ public void setValues(Map updates) { viewModel.getAssetDefinitionService().addLocalRefs(args); //rebuild the view - tokenView.displayTicketHolder(token, data, viewModel.getAssetDefinitionService(), false); + tokenView.displayTicketHolder(token, data, viewModel.getAssetDefinitionService(), ViewType.VIEW); } } @@ -434,12 +448,19 @@ public void dismissed(String txHash, long callbackId, boolean actionCompleted) setResult(RESULT_OK, intent); finish(); } + else + { + viewModel.track(Analytics.Action.ACTION_SHEET_CANCELLED, confirmationDialogProps); + } } @Override public void notifyConfirm(String mode) { viewModel.actionSheetConfirm(mode); + + confirmationDialogProps.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, confirmationDialogProps); } ActivityResultLauncher getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenManagementActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenManagementActivity.java index e3978f612e..4c862d3486 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenManagementActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenManagementActivity.java @@ -1,6 +1,7 @@ package com.alphawallet.app.ui; import static com.alphawallet.app.C.ADDED_TOKEN; +import static com.alphawallet.app.C.RESET_WALLET; import static com.alphawallet.app.repository.TokensRealmSource.ADDRESS_FORMAT; import android.content.Intent; @@ -76,7 +77,15 @@ public void afterTextChanged(final Editable s) final ActivityResultLauncher addTokenLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getData() == null) return; - tokenUpdates = result.getData().getParcelableArrayListExtra(ADDED_TOKEN); + boolean saved = result.getData().getBooleanExtra(RESET_WALLET, false); + if (saved) + { + //finish and return + Intent intent = new Intent(); + intent.putExtra(RESET_WALLET, true); + setResult(RESULT_OK, intent); + finish(); + } }); @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java index 3d4b1b4184..7f7d525f08 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java @@ -1,5 +1,10 @@ package com.alphawallet.app.ui; +import static com.alphawallet.app.C.Key.WALLET; +import static com.alphawallet.app.ui.widget.holder.TransactionHolder.TRANSACTION_BALANCE_PRECISION; +import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; + import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; @@ -16,14 +21,16 @@ import androidx.appcompat.app.ActionBar; import androidx.lifecycle.ViewModelProvider; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionData; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; @@ -33,7 +40,6 @@ import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.ActionSheetDialog; -import com.alphawallet.app.widget.ActionSheetMode; import com.alphawallet.app.widget.ChainName; import com.alphawallet.app.widget.CopyTextView; import com.alphawallet.app.widget.FunctionButtonBar; @@ -41,24 +47,16 @@ import com.alphawallet.app.widget.TokenIcon; import com.alphawallet.token.tools.Numeric; +import org.web3j.crypto.Keys; + import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import javax.inject.Inject; - -import timber.log.Timber; - -import static com.alphawallet.app.C.Key.WALLET; -import static com.alphawallet.app.ui.widget.holder.TransactionHolder.TRANSACTION_BALANCE_PRECISION; -import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; -import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; - -import org.web3j.crypto.Keys; - import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; @AndroidEntryPoint public class TransactionDetailActivity extends BaseActivity implements StandardFunctionInterface, ActionSheetCallback @@ -447,7 +445,12 @@ public ActivityResultLauncher gasSelectLauncher() } @Override - public void notifyConfirm(String mode) { viewModel.actionSheetConfirm(mode); } + public void notifyConfirm(String mode) + { + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, props); + } private void txWritten(TransactionData transactionData) { diff --git a/app/src/main/java/com/alphawallet/app/ui/TransferNFTActivity.java b/app/src/main/java/com/alphawallet/app/ui/TransferNFTActivity.java index f98dedcaed..b4bf33b8f6 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TransferNFTActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TransferNFTActivity.java @@ -9,7 +9,6 @@ import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; -import android.util.Log; import android.util.Pair; import android.view.View; import android.view.WindowManager; @@ -25,6 +24,8 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.EnsNodeNotSyncCallback; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.SignAuthenticationCallback; @@ -34,7 +35,7 @@ import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.service.GasService; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.ui.widget.adapter.NonFungibleTokenAdapter; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; @@ -55,7 +56,6 @@ import com.alphawallet.token.tools.Numeric; import org.jetbrains.annotations.NotNull; -import org.web3j.protocol.core.methods.response.EthEstimateGas; import java.math.BigDecimal; import java.math.BigInteger; @@ -63,8 +63,6 @@ import java.util.Collections; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -117,7 +115,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) addressInput = findViewById(R.id.input_address); addressInput.setAddressCallback(this); - addressInput.setEnsNodeNotSyncCallback(this); + //addressInput.setEnsNodeNotSyncCallback(this); sendAddress = null; ensAddress = null; @@ -260,7 +258,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) addressInput.setAddress(extracted_address); } break; - case QRScanner.DENY_PERMISSION: + case QRScannerActivity.DENY_PERMISSION: showCameraDenied(); break; default: @@ -270,7 +268,6 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) break; } break; - case C.COMPLETED_TRANSACTION: Intent i = new Intent(); i.putExtra(C.EXTRA_TXHASH, data.getStringExtra(C.EXTRA_TXHASH)); @@ -428,7 +425,9 @@ public void dismissed(String txHash, long callbackId, boolean actionCompleted) @Override public void notifyConfirm(String mode) { - viewModel.actionSheetConfirm(mode); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, props); } ActivityResultLauncher getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), @@ -522,7 +521,7 @@ public void onNodeNotSynced() dialog.setButtonListener(v -> dialog.dismiss()); dialog.setSecondaryButtonText(R.string.ignore); dialog.setSecondaryButtonListener(v -> { - addressInput.setEnsHandlerNodeSyncFlag(false); // skip node sync check + //addressInput.setEnsHandlerNodeSyncFlag(false); // skip node sync check // re enter current input to resolve again String currentInput = addressInput.getEditText().getText().toString(); addressInput.getEditText().setText(""); diff --git a/app/src/main/java/com/alphawallet/app/ui/TransferTicketDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/TransferTicketDetailActivity.java index bd868600ce..4aa9aefc67 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TransferTicketDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TransferTicketDetailActivity.java @@ -20,7 +20,6 @@ import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; -import android.util.Log; import android.view.View; import android.view.WindowManager; import android.widget.EditText; @@ -40,6 +39,8 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.DisplayState; import com.alphawallet.app.entity.ErrorEnvelope; @@ -50,7 +51,7 @@ import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.service.GasService; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.ui.widget.adapter.NonFungibleTokenAdapter; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; @@ -72,7 +73,6 @@ import com.alphawallet.token.tools.Numeric; import org.jetbrains.annotations.NotNull; -import org.web3j.protocol.core.methods.response.EthEstimateGas; import java.math.BigDecimal; import java.math.BigInteger; @@ -85,8 +85,6 @@ import java.util.List; import java.util.Locale; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -98,47 +96,39 @@ */ @AndroidEntryPoint public class TransferTicketDetailActivity extends BaseActivity - implements TokensAdapterCallback, StandardFunctionInterface, AddressReadyCallback, ActionSheetCallback { - private static final int SEND_INTENT_REQUEST_CODE = 2; - + implements TokensAdapterCallback, StandardFunctionInterface, AddressReadyCallback, ActionSheetCallback +{ protected TransferTicketDetailViewModel viewModel; private SystemView systemView; private ProgressView progressView; private AWalletAlertDialog dialog; private FunctionButtonBar functionBar; - private Token token; private NonFungibleTokenAdapter adapter; - private TextView validUntil; private TextView textQuantity; - private String ticketIds; private List selection; private DisplayState transferStatus; - private InputAddress addressInput; private String sendAddress; private String ensAddress; - private ActionSheetDialog actionDialog; private AWalletConfirmationDialog confirmationDialog; - private AppCompatRadioButton pickLink; private AppCompatRadioButton pickTransfer; - private LinearLayout pickTicketQuantity; private LinearLayout pickTransferMethod; private LinearLayout pickExpiryDate; private LinearLayout buttonLinkPick; private LinearLayout buttonTransferPick; - private EditText expiryDateEditText; private EditText expiryTimeEditText; private DatePickerDialog datePickerDialog; private TimePickerDialog timePickerDialog; - private SignAuthenticationCallback signCallback; + private ActivityResultLauncher getGasSettings; + private ActivityResultLauncher transferLinkFinalResult; @Nullable private Disposable calcGasCost; @@ -147,19 +137,44 @@ public class TransferTicketDetailActivity extends BaseActivity protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.activity_transfer_detail); - viewModel = new ViewModelProvider(this) - .get(TransferTicketDetailViewModel.class); + toolbar(); + + getIntentData(); + + initViewModel(); + + initViews(); + initResultLaunchers(); + + setupScreen(); + } + + private void getIntentData() + { long chainId = getIntent().getLongExtra(C.EXTRA_CHAIN_ID, MAINNET_ID); token = viewModel.getTokenService().getToken(chainId, getIntent().getStringExtra(C.EXTRA_ADDRESS)); - ticketIds = getIntent().getStringExtra(EXTRA_TOKENID_LIST); transferStatus = DisplayState.values()[getIntent().getIntExtra(EXTRA_STATE, 0)]; selection = token.stringHexToBigIntegerList(ticketIds); + } - toolbar(); + private void initResultLaunchers() + { + getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> actionDialog.setCurrentGasIndex(result)); + + transferLinkFinalResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> { + LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(PRUNE_ACTIVITY)); //TODO: implement prune via result codes + }); + } + + private void initViews() + { systemView = findViewById(R.id.system_view); systemView.hide(); progressView = findViewById(R.id.progress_view); @@ -171,14 +186,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) sendAddress = null; ensAddress = null; - viewModel.progress().observe(this, systemView::showProgress); - viewModel.queueProgress().observe(this, progressView::updateProgress); - viewModel.pushToast().observe(this, this::displayToast); - viewModel.newTransaction().observe(this, this::onTransaction); - viewModel.error().observe(this, this::onError); - viewModel.universalLinkReady().observe(this, this::linkReady); - viewModel.transactionFinalised().observe(this, this::txWritten); - viewModel.transactionError().observe(this, this::txError); //we should import a token and a list of chosen ids RecyclerView list = findViewById(R.id.listTickets); adapter = new NonFungibleTokenAdapter(null, token, selection, viewModel.getAssetDefinitionService()); @@ -194,39 +201,47 @@ protected void onCreate(@Nullable Bundle savedInstanceState) functionBar = findViewById(R.id.layoutButtons); expiryDateEditText = findViewById(R.id.edit_expiry_date); - expiryDateEditText.addTextChangedListener(new TextWatcher() { + expiryDateEditText.addTextChangedListener(new TextWatcher() + { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + public void beforeTextChanged(CharSequence s, int start, int count, int after) + { } @Override @SuppressLint("StringFormatInvalid") - public void onTextChanged(CharSequence s, int start, int before, int count) { + public void onTextChanged(CharSequence s, int start, int before, int count) + { validUntil.setText(getString(R.string.link_valid_until, s.toString(), expiryTimeEditText.getText().toString())); } @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(Editable s) + { } }); expiryTimeEditText = findViewById(R.id.edit_expiry_time); - expiryTimeEditText.addTextChangedListener(new TextWatcher() { + expiryTimeEditText.addTextChangedListener(new TextWatcher() + { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + public void beforeTextChanged(CharSequence s, int start, int count, int after) + { } @SuppressLint("StringFormatInvalid") @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { + public void onTextChanged(CharSequence s, int start, int before, int count) + { validUntil.setText(getString(R.string.link_valid_until, expiryDateEditText.getText().toString(), s.toString())); } @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(Editable s) + { } }); @@ -239,17 +254,31 @@ public void afterTextChanged(Editable s) { functionBar.setupFunctions(this, new ArrayList<>(Collections.singletonList(R.string.action_next))); functionBar.revealButtons(); + } - setupScreen(); + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(TransferTicketDetailViewModel.class); + viewModel.progress().observe(this, systemView::showProgress); + viewModel.queueProgress().observe(this, progressView::updateProgress); + viewModel.pushToast().observe(this, this::displayToast); + viewModel.newTransaction().observe(this, this::onTransaction); + viewModel.error().observe(this, this::onError); + viewModel.universalLinkReady().observe(this, this::linkReady); + viewModel.transactionFinalised().observe(this, this::txWritten); + viewModel.transactionError().observe(this, this::txError); } //TODO: This is repeated code also in SellDetailActivity. Probably should be abstracted out into generic view code routine - private void initQuantitySelector() { + private void initQuantitySelector() + { pickTicketQuantity.setVisibility(View.VISIBLE); RelativeLayout plusButton = findViewById(R.id.layout_quantity_add); plusButton.setOnClickListener(v -> { int quantity = Integer.parseInt(textQuantity.getText().toString()); - if ((quantity + 1) <= adapter.getTicketRangeCount()) { + if ((quantity + 1) <= adapter.getTicketRangeCount()) + { quantity++; textQuantity.setText(String.valueOf(quantity)); selection = token.pruneIDList(ticketIds, quantity); @@ -259,7 +288,8 @@ private void initQuantitySelector() { RelativeLayout minusButton = findViewById(R.id.layout_quantity_minus); minusButton.setOnClickListener(v -> { int quantity = Integer.parseInt(textQuantity.getText().toString()); - if ((quantity - 1) > 0) { + if ((quantity - 1) > 0) + { quantity--; textQuantity.setText(String.valueOf(quantity)); selection = token.pruneIDList(ticketIds, 1); @@ -352,7 +382,7 @@ public void gotAuthorisation(boolean gotAuth) if (gotAuth) { - if(token.isERC721Ticket()) + if (token.isERC721Ticket()) { viewModel.generateSpawnLink(selection, token.getAddress(), calculateExpiryTime()); } @@ -525,7 +555,8 @@ protected void onDestroy() } @Override - public void onTokenClick(View view, Token token, List ids, boolean selection) { + public void onTokenClick(View view, Token token, List ids, boolean selection) + { Context context = view.getContext(); //TODO: what action should be performed when clicking on a range? } @@ -571,12 +602,12 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) addressInput.setAddress(extracted_address); } break; - case QRScanner.DENY_PERMISSION: + case QRScannerActivity.DENY_PERMISSION: showCameraDenied(); break; default: Timber.tag("SEND").e(String.format(getString(R.string.barcode_error_format), - "Code: " + resultCode + "Code: " + resultCode )); break; } @@ -589,7 +620,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) finish(); break; case SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS: - if (actionDialog != null && actionDialog.isShowing()) actionDialog.completeSignRequest(resultCode == RESULT_OK); + if (actionDialog != null && actionDialog.isShowing()) + actionDialog.completeSignRequest(resultCode == RESULT_OK); //signCallback.gotAuthorisation(resultCode == RESULT_OK); break; @@ -614,7 +646,7 @@ private void showCameraDenied() private void linkReady(String universalLink) { int quantity = 1; - if(selection != null) + if (selection != null) { quantity = selection.size(); } @@ -684,11 +716,6 @@ public void cancelAuthentication() confirmationDialog.show(); } - ActivityResultLauncher transferLinkFinalResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> { - LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(PRUNE_ACTIVITY)); //TODO: implement prune via result codes - }); - private void transferLinkFinal(String universalLink) { //create share intent @@ -728,7 +755,7 @@ private void initTimePicker() //set for now String time = String.format(Locale.getDefault(), "%02d:%02d", Calendar.getInstance().get(Calendar.HOUR_OF_DAY), - Calendar.getInstance().get(Calendar.MINUTE)); + Calendar.getInstance().get(Calendar.MINUTE)); expiryTimeEditText.setText(time); } @@ -781,7 +808,7 @@ private void calculateTransactionCost() final String txSendAddress = sendAddress; sendAddress = null; - final byte[] transactionBytes = viewModel.getERC721TransferBytes(txSendAddress,token.getAddress(),ticketIds, token.tokenInfo.chainId); + final byte[] transactionBytes = viewModel.getERC721TransferBytes(txSendAddress, token.getAddress(), ticketIds, token.tokenInfo.chainId); if (token.isEthereum()) { checkConfirm(BigInteger.valueOf(GAS_LIMIT_MIN), transactionBytes, txSendAddress, txSendAddress); @@ -804,7 +831,6 @@ private void handleError(Throwable throwable, final byte[] transactionBytes, fin checkConfirm(BigInteger.ZERO, transactionBytes, txSendAddress, resolvedAddress); } - private void calculateEstimateDialog() { if (dialog != null && dialog.isShowing()) dialog.dismiss(); @@ -819,7 +845,8 @@ private void calculateEstimateDialog() /** * Called to check if we're ready to send user to confirm screen / activity sheet popup */ - private void checkConfirm(final BigInteger sendGasLimit, final byte[] transactionBytes, final String txSendAddress, final String resolvedAddress) { + private void checkConfirm(final BigInteger sendGasLimit, final byte[] transactionBytes, final String txSendAddress, final String resolvedAddress) + { Web3Transaction w3tx = new Web3Transaction( new Address(txSendAddress), @@ -869,7 +896,8 @@ public void sendTransaction(Web3Transaction finalTx) public void dismissed(String txHash, long callbackId, boolean actionCompleted) { //ActionSheet was dismissed - if (!TextUtils.isEmpty(txHash)) { + if (!TextUtils.isEmpty(txHash)) + { Intent intent = new Intent(); intent.putExtra(C.EXTRA_TXHASH, txHash); setResult(RESULT_OK, intent); @@ -878,10 +906,12 @@ public void dismissed(String txHash, long callbackId, boolean actionCompleted) } @Override - public void notifyConfirm(String mode) { viewModel.actionSheetConfirm(mode); } - - ActivityResultLauncher getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> actionDialog.setCurrentGasIndex(result)); + public void notifyConfirm(String mode) + { + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, props); + } @Override public ActivityResultLauncher gasSelectLauncher() diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletActionsActivity.java b/app/src/main/java/com/alphawallet/app/ui/WalletActionsActivity.java index 972cebf510..c337f854f4 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletActionsActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletActionsActivity.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; @@ -36,8 +37,6 @@ import com.alphawallet.app.widget.SettingsItemView; import com.alphawallet.app.widget.UserAvatar; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint @@ -105,6 +104,14 @@ private void initViewModel() { } } + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + getMenuInflater().inflate(R.menu.menu_wallet_manage, menu); + + return super.onCreateOptionsMenu(menu); + } + private void onTaskStatusChanged(Boolean isTaskRunning) { } @@ -123,6 +130,14 @@ public boolean onOptionsItemSelected(MenuItem item) onBackPressed(); return true; } + else if (item.getItemId() == R.id.action_key_status) + { + //show the key status + Intent intent = new Intent(this, WalletDiagnosticActivity.class); + intent.putExtra("wallet", wallet); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + } return super.onOptionsItemSelected(item); } @@ -271,7 +286,10 @@ private void confirmDelete(Wallet wallet) { result -> { if (result.getResultCode() == RESULT_OK) { - successOverlay.setVisibility(View.VISIBLE); + if (successOverlay != null) + { + successOverlay.setVisibility(View.VISIBLE); + } handler.postDelayed(this, 1000); backupSuccessful(); finish(); diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletConnectActivity.java b/app/src/main/java/com/alphawallet/app/ui/WalletConnectActivity.java index ded1116977..1694952526 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletConnectActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletConnectActivity.java @@ -30,6 +30,8 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.DAppFunction; import com.alphawallet.app.entity.NetworkInfo; @@ -37,11 +39,15 @@ import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.analytics.ActionSheetSource; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.walletconnect.WCRequest; import com.alphawallet.app.repository.EthereumNetworkBase; +import com.alphawallet.app.repository.SignRecord; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; import com.alphawallet.app.viewmodel.WalletConnectViewModel; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.alphawallet.app.walletconnect.WCClient; import com.alphawallet.app.walletconnect.WCSession; import com.alphawallet.app.walletconnect.entity.WCEthereumSignMessage; @@ -53,7 +59,9 @@ import com.alphawallet.app.web3.entity.WalletAddEthereumChainObject; import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.widget.ActionSheet; import com.alphawallet.app.widget.ActionSheetDialog; +import com.alphawallet.app.widget.ActionSheetSignDialog; import com.alphawallet.app.widget.ChainName; import com.alphawallet.app.widget.FunctionButtonBar; import com.alphawallet.app.widget.SignTransactionDialog; @@ -64,44 +72,42 @@ import com.alphawallet.token.entity.Signable; import com.bumptech.glide.Glide; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import org.jetbrains.annotations.NotNull; import org.web3j.utils.Numeric; -import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.UUID; -import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import kotlin.Unit; -import okhttp3.OkHttpClient; import timber.log.Timber; @AndroidEntryPoint -public class WalletConnectActivity extends BaseActivity implements ActionSheetCallback, StandardFunctionInterface, WalletConnectCallback { +public class WalletConnectActivity extends BaseActivity implements ActionSheetCallback, StandardFunctionInterface, WalletConnectCallback +{ public static final String WC_LOCAL_PREFIX = "wclocal:"; public static final String WC_INTENT = "wcintent:"; private static final String TAG = "WCClient"; private static final String DEFAULT_IDON = "https://example.walletconnect.org/favicon.ico"; private static final long CONNECT_TIMEOUT = 10 * DateUtils.SECOND_IN_MILLIS; // 10 Seconds timeout private final Handler handler = new Handler(Looper.getMainLooper()); - private final LocalBroadcastManager broadcastManager; - WalletConnectViewModel viewModel; + private final long switchChainDialogCallbackId = 1; + private WalletConnectViewModel viewModel; + private LocalBroadcastManager broadcastManager; private WCClient client; private WCSession session; private WCPeerMeta peerMeta; private WCPeerMeta remotePeerMeta; - private ActionSheetDialog confirmationDialog; + private ActionSheet confirmationDialog; ActivityResultLauncher getGasSettings = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> confirmationDialog.setCurrentGasIndex(result)); private AddEthereumChainPrompt addEthereumChainPrompt; - private final long switchChainDialogCallbackId = 1; // data for switch chain request private long switchChainRequestId; // rpc request id private long switchChainId; // new chain to switch to @@ -113,10 +119,12 @@ public class WalletConnectActivity extends BaseActivity implements ActionSheetCa private TextView peerUrl; private TextView statusText; private TextView textName; + private TextView txCount; private ChainName chainName; private TokenIcon chainIcon; private ProgressBar progressBar; private LinearLayout infoLayout; + private LinearLayout txCountLayout; private FunctionButtonBar functionBar; private boolean fromDappBrowser = false; //if using this from dappBrowser (which is a bit strange but could happen) then return back to browser once signed private boolean fromPhoneBrowser = false; //if from phone browser, clicking 'back' should take user back to dapp running on the phone's browser, @@ -128,6 +136,10 @@ public class WalletConnectActivity extends BaseActivity implements ActionSheetCa private String signData; private WCEthereumSignMessage.WCSignType signType; private long chainIdOverride; + + @Inject + AWWalletConnectClient awWalletConnectClient; + ActivityResultLauncher getNetwork = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getData() == null) return; @@ -138,7 +150,8 @@ public class WalletConnectActivity extends BaseActivity implements ActionSheetCa private boolean waitForWalletConnectSession = false; private long requestId = 0; private AWalletAlertDialog dialog = null; - private final BroadcastReceiver walletConnectActionReceiver = new BroadcastReceiver() { + private final BroadcastReceiver walletConnectActionReceiver = new BroadcastReceiver() + { @Override public void onReceive(Context context, Intent intent) { @@ -183,16 +196,10 @@ public void onReceive(Context context, Intent intent) Timber.tag(TAG).d("MSG: ADD CHAIN"); onAddChainRequest(intent); break; - } } }; - public WalletConnectActivity() - { - broadcastManager = LocalBroadcastManager.getInstance(this); - } - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -408,6 +415,8 @@ private void initViews() peerUrl = findViewById(R.id.peer_url); statusText = findViewById(R.id.connection_status); textName = findViewById(R.id.text_name); + txCountLayout = findViewById(R.id.layout_tx_count); + txCount = findViewById(R.id.tx_count); chainName = findViewById(R.id.chain_name); chainIcon = findViewById(R.id.chain_icon); @@ -587,6 +596,7 @@ private void startMessageCheck() filter.addAction(C.WALLET_CONNECT_CLIENT_TERMINATE); filter.addAction(C.WALLET_CONNECT_SWITCH_CHAIN); filter.addAction(C.WALLET_CONNECT_ADD_CHAIN); + if (broadcastManager == null) broadcastManager = LocalBroadcastManager.getInstance(this); broadcastManager.registerReceiver(walletConnectActionReceiver, filter); } @@ -647,6 +657,8 @@ private void invalidSession() }); dialog.setCancelable(false); dialog.show(); + + viewModel.trackError(Analytics.Error.WALLET_CONNECT, getString(R.string.invalid_walletconnect_session)); } private void initWalletConnectPeerMeta() @@ -680,8 +692,8 @@ protected void onSaveInstanceState(@NotNull Bundle state) state.putString("SESSIONIDSTR", getSessionId()); if (confirmationDialog != null && confirmationDialog.isShowing() && confirmationDialog.getTransaction() != null) { - state.putParcelable("TRANSACTION", confirmationDialog.getTransaction()); - state.putLong("CHAINID", viewModel.getChainId(getSessionId())); + state.putParcelable("TRANSACTION", confirmationDialog.getTransaction()); + state.putLong("CHAINID", viewModel.getChainId(getSessionId())); } if (confirmationDialog != null && confirmationDialog.isShowing() && signData != null) { @@ -736,6 +748,7 @@ private void displaySessionStatus(String sessionId) chainIcon.setVisibility(View.VISIBLE); chainIcon.bindData(viewModel.getChainId(sessionId)); viewModel.startGasCycle(viewModel.getChainId(sessionId)); + updateSignCount(); } } @@ -768,6 +781,7 @@ private void onSessionRequest(Long id, WCPeerMeta peer, long chainId) peerName.setText(peer.getName()); textName.setText(peer.getName()); peerUrl.setText(peer.getUrl()); + txCount.setText(R.string.empty); chainName.setChainID(chainIdOverride); chainIcon.setVisibility(View.VISIBLE); chainIcon.bindData(chainIdOverride); @@ -776,6 +790,8 @@ private void onSessionRequest(Long id, WCPeerMeta peer, long chainId) confirmationDialog = new ActionSheetDialog(this, peer, chainId, displayIcon, this); confirmationDialog.show(); confirmationDialog.fullExpand(); + + viewModel.track(Analytics.Action.WALLET_CONNECT_SESSION_REQUEST); } private void onEthSign(Long id, WCEthereumSignMessage message) @@ -819,79 +835,47 @@ private void onFailure(@NonNull Throwable throwable) }); dialog.setCancelable(false); dialog.show(); + + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ERROR_MESSAGE, throwable.getMessage()); + viewModel.track(Analytics.Action.WALLET_CONNECT_TRANSACTION_FAILED, props); } private void doSignMessage(final Signable signable) { - final DAppFunction dappFunction = new DAppFunction() { - @Override - public void DAppError(Throwable error, Signable message) - { - showErrorDialog(error.getMessage()); - confirmationDialog.dismiss(); - if (fromDappBrowser) switchToDappBrowser(); - requestId = 0; - lastId = 0; - signData = null; - } - - @Override - public void DAppReturn(byte[] data, Signable message) - { - //store sign - viewModel.recordSign(signable, getSessionId(), () -> { - viewModel.approveRequest(getApplication(), getSessionId(), message.getCallbackId(), Numeric.toHexString(data)); - confirmationDialog.success(); - if (fromDappBrowser) - { - confirmationDialog.forceDismiss(); - switchToDappBrowser(); - } - requestId = 0; - lastId = 0; - signData = null; - }); - } - }; - - signCallback = new SignAuthenticationCallback() { - @Override - public void gotAuthorisation(boolean gotAuth) - { - viewModel.signMessage( - signable, - dappFunction); - } + confirmationDialog = new ActionSheetSignDialog(this, this, signable); + confirmationDialog.show(); - @Override - public void gotAuthorisationForSigning(boolean gotAuth, Signable messageToSign) - { - if (gotAuth) - { - viewModel.signMessage( - signable, - dappFunction); - } - else - { - cancelAuthentication(); - } - } + viewModel.track(Analytics.Action.WALLET_CONNECT_SIGN_MESSAGE_REQUEST); + } - @Override - public void cancelAuthentication() + @Override + public void signingComplete(SignatureFromKey signature, Signable signable) + { + viewModel.recordSign(signable, getSessionId(), () -> { + viewModel.approveRequest(getApplication(), getSessionId(), signable.getCallbackId(), Numeric.toHexString(signature.signature)); + confirmationDialog.success(); + if (fromDappBrowser) { - requestId = 0; - showErrorDialogCancel(getString(R.string.title_dialog_error), getString(R.string.message_authentication_failed)); - viewModel.rejectRequest(getApplication(), getSessionId(), lastId, getString(R.string.message_authentication_failed)); - confirmationDialog.dismiss(); - if (fromDappBrowser) switchToDappBrowser(); + confirmationDialog.forceDismiss(); + switchToDappBrowser(); } - }; + requestId = 0; + lastId = 0; + signData = null; + updateSignCount(); + }); + } - confirmationDialog = new ActionSheetDialog(this, this, signCallback, signable); - confirmationDialog.setCanceledOnTouchOutside(false); - confirmationDialog.show(); + @Override + public void signingFailed(Throwable error, Signable message) + { + showErrorDialog(error.getMessage()); + confirmationDialog.dismiss(); + if (fromDappBrowser) switchToDappBrowser(); + requestId = 0; + lastId = 0; + signData = null; } private void onEthSignTransaction(Long id, WCEthereumTransaction transaction, long chainId) @@ -904,6 +888,8 @@ private void onEthSignTransaction(Long id, WCEthereumTransaction transaction, lo confirmationDialog = confDialog; confirmationDialog.setSignOnly(); //sign transaction only confirmationDialog.show(); + + viewModel.track(Analytics.Action.WALLET_CONNECT_SIGN_TRANSACTION_REQUEST); } } @@ -916,6 +902,8 @@ private void onEthSendTransaction(Long id, WCEthereumTransaction transaction, lo { confirmationDialog = confDialog; confirmationDialog.show(); + + viewModel.track(Analytics.Action.WALLET_CONNECT_SEND_TRANSACTION_REQUEST); } } @@ -935,8 +923,7 @@ private ActionSheetDialog generateTransactionRequest(Web3Transaction w3Tx, long confDialog.setCanceledOnTouchOutside(false); confDialog.waitForEstimate(); - viewModel.calculateGasEstimate(viewModel.getWallet(), Numeric.hexStringToByteArray(w3Tx.payload), - chainId, w3Tx.recipient.toString(), new BigDecimal(w3Tx.value), w3Tx.gasLimit) + viewModel.calculateGasEstimate(viewModel.getWallet(), w3Tx, chainId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(confDialog::setGasEstimate, @@ -959,8 +946,10 @@ private void killSession() Timber.tag(TAG).d(": Terminate Session: %s", getSessionId()); if (client != null && session != null && client.isConnected()) { + viewModel.track(Analytics.Action.WALLET_CONNECT_SESSION_ENDED); client.killSession(); viewModel.disconnectSession(this, client.sessionId()); + awWalletConnectClient.updateNotification(); handler.postDelayed(this::finish, 5000); } else @@ -980,6 +969,7 @@ public void onPause() public void onResume() { super.onResume(); + viewModel.track(Analytics.Navigation.WALLET_CONNECT_SESSION_DETAIL); //see if the session is active setupClient(getSessionId()); startMessageCheck(); @@ -1008,6 +998,8 @@ private void showTimeoutDialog() }); dialog.setCancelable(false); dialog.show(); + + viewModel.track(Analytics.Action.WALLET_CONNECT_CONNECTION_TIMEOUT); }); } } @@ -1028,6 +1020,8 @@ private void showErrorDialog(String message) }); dialog.setCancelable(false); dialog.show(); + + viewModel.trackError(Analytics.Error.WALLET_CONNECT, message); }); } } @@ -1044,6 +1038,8 @@ private void showErrorDialogCancel(String title, String message) dialog.setButton(R.string.action_cancel, v -> dialog.dismiss()); dialog.setCancelable(false); dialog.show(); + + viewModel.trackError(Analytics.Error.WALLET_CONNECT, message); }); } } @@ -1100,6 +1096,8 @@ private void showErrorDialogTerminate(String message) }); dialog.setCancelable(false); dialog.show(); + + viewModel.trackError(Analytics.Error.WALLET_CONNECT, message); }); } } @@ -1186,7 +1184,8 @@ public void getAuthorisation(SignAuthenticationCallback callback) @Override public void sendTransaction(Web3Transaction finalTx) { - final SendTransactionInterface callback = new SendTransactionInterface() { + final SendTransactionInterface callback = new SendTransactionInterface() + { @Override public void transactionSuccess(Web3Transaction web3Tx, String hashData) { @@ -1196,6 +1195,9 @@ public void transactionSuccess(Web3Transaction web3Tx, String hashData) if (fromDappBrowser) switchToDappBrowser(); confirmationDialog.transactionWritten(hashData); requestId = 0; + updateSignCount(); + + viewModel.track(Analytics.Action.WALLET_CONNECT_TRANSACTION_SUCCESS); } @Override @@ -1225,6 +1227,10 @@ private void displayTransactionError(final Throwable throwable) dialog.setButtonText(R.string.button_ok); dialog.setButtonListener(v -> dialog.dismiss()); dialog.show(); + + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ERROR_MESSAGE, throwable.getMessage()); + viewModel.track(Analytics.Action.WALLET_CONNECT_TRANSACTION_FAILED, props); }); } } @@ -1236,6 +1242,7 @@ public void dismissed(String txHash, long callbackId, boolean actionCompleted) if (!actionCompleted) { viewModel.rejectRequest(this, getSessionId(), callbackId, getString(R.string.message_reject_request)); + viewModel.track(Analytics.Action.WALLET_CONNECT_TRANSACTION_CANCELLED); } if (fromDappBrowser) switchToDappBrowser(); @@ -1246,7 +1253,10 @@ public void dismissed(String txHash, long callbackId, boolean actionCompleted) @Override public void notifyConfirm(String mode) { - viewModel.actionSheetConfirm(mode); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + props.put(Analytics.PROPS_ACTION_SHEET_SOURCE, ActionSheetSource.WALLET_CONNECT.getValue()); + viewModel.track(Analytics.Action.ACTION_SHEET_COMPLETED, props); } @Override @@ -1258,7 +1268,8 @@ public ActivityResultLauncher gasSelectLauncher() @Override public void signTransaction(Web3Transaction tx) { - DAppFunction dappFunction = new DAppFunction() { + DAppFunction dappFunction = new DAppFunction() + { @Override public void DAppError(Throwable error, Signable message) { @@ -1276,10 +1287,11 @@ public void DAppReturn(byte[] data, Signable message) confirmationDialog.transactionWritten(getString(R.string.dialog_title_sign_transaction)); if (fromDappBrowser) switchToDappBrowser(); requestId = 0; + updateSignCount(); } }; - viewModel.signTransaction(getBaseContext(), tx, dappFunction, peerUrl.getText().toString(), viewModel.getChainId(getSessionId())); + viewModel.signTransaction(getBaseContext(), tx, dappFunction, peerUrl.getText().toString(), viewModel.getChainId(getSessionId()), viewModel.defaultWallet().getValue()); if (fromDappBrowser) switchToDappBrowser(); } @@ -1299,6 +1311,7 @@ public void notifyWalletConnectApproval(long selectedChain) infoLayout.setVisibility(View.VISIBLE); chainIdOverride = selectedChain; setupClient(getSessionId()); //should populate this activity + viewModel.track(Analytics.Action.WALLET_CONNECT_SESSION_APPROVED); if (fromDappBrowser) { //switch back to dappBrowser @@ -1310,6 +1323,7 @@ public void notifyWalletConnectApproval(long selectedChain) public void denyWalletConnect() { client.rejectSession(getString(R.string.message_reject_request)); + viewModel.track(Analytics.Action.WALLET_CONNECT_SESSION_REJECTED); finish(); } @@ -1342,6 +1356,8 @@ private void showSwitchChainDialog() confirmationDialog.setCanceledOnTouchOutside(false); confirmationDialog.show(); confirmationDialog.fullExpand(); + + viewModel.track(Analytics.Action.WALLET_CONNECT_SWITCH_NETWORK_REQUEST); } catch (Exception e) { @@ -1440,4 +1456,19 @@ public void buttonClick(long callbackId, Token baseToken) displaySessionStatus(session.getTopic()); } } + + private void updateSignCount() + { + ArrayList recordList = viewModel.getSignRecords(getSessionId()); + txCount.setText(String.valueOf(recordList.size())); + if (recordList.size() > 0) + { + txCountLayout.setOnClickListener(v -> { + Intent intent = new Intent(getApplication(), SignDetailActivity.class); + intent.putParcelableArrayListExtra(C.EXTRA_STATE, recordList); + intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + startActivity(intent); + }); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletConnectNotificationActivity.java b/app/src/main/java/com/alphawallet/app/ui/WalletConnectNotificationActivity.java new file mode 100644 index 0000000000..8f1b7acd35 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/WalletConnectNotificationActivity.java @@ -0,0 +1,52 @@ +package com.alphawallet.app.ui; + +import android.content.Intent; +import android.os.Bundle; + +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.interact.WalletConnectInteract; + +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; + +import dagger.hilt.android.AndroidEntryPoint; + +/** + * This activity is created to simplify notification click event, according to sessions count, when: + * 1: to session details + * more than 1: to sessions list + */ +@AndroidEntryPoint +public class WalletConnectNotificationActivity extends BaseActivity +{ + @Inject + WalletConnectInteract walletConnectInteract; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + route(); + finish(); + } + + private void route() + { + Intent intent; + List sessions = walletConnectInteract.getSessions(); + if (sessions.size() == 1) + { + intent = WalletConnectSessionActivity.newIntent(getApplicationContext(), sessions.get(0)); + } + else + { + intent = new Intent(getApplicationContext(), WalletConnectSessionActivity.class); + } + + startActivity(intent); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletConnectSessionActivity.java b/app/src/main/java/com/alphawallet/app/ui/WalletConnectSessionActivity.java index 20d42d484d..4b33f2df2c 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletConnectSessionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletConnectSessionActivity.java @@ -1,6 +1,6 @@ package com.alphawallet.app.ui; -import static com.alphawallet.app.C.Key.WALLET; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import android.content.BroadcastReceiver; import android.content.Context; @@ -9,6 +9,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -21,6 +22,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; import androidx.lifecycle.ViewModelProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; @@ -28,17 +31,22 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; -import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.entity.walletconnect.WalletConnectV2SessionItem; import com.alphawallet.app.repository.EthereumNetworkRepository; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; +import com.alphawallet.app.ui.widget.divider.ListDivider; import com.alphawallet.app.viewmodel.WalletConnectViewModel; -import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.bumptech.glide.Glide; import java.util.List; +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; +import timber.log.Timber; /** @@ -48,15 +56,13 @@ public class WalletConnectSessionActivity extends BaseActivity { private final Handler handler = new Handler(Looper.getMainLooper()); - private final LocalBroadcastManager broadcastManager; + private LocalBroadcastManager broadcastManager; WalletConnectViewModel viewModel; private RecyclerView recyclerView; private Button btnConnectWallet; private LinearLayout layoutNoActiveSessions; private CustomAdapter adapter; - private Wallet wallet; private List wcSessions; - private int connectionCount = -1; private final BroadcastReceiver walletConnectChangeReceiver = new BroadcastReceiver() { @Override @@ -66,27 +72,25 @@ public void onReceive(Context context, Intent intent) if (action.equals(C.WALLET_CONNECT_COUNT_CHANGE)) { handler.post(() -> adapter.notifyDataSetChanged()); - connectionCount = intent.getIntExtra("count", 0); } } }; - public WalletConnectSessionActivity() - { - broadcastManager = LocalBroadcastManager.getInstance(this); - } + @Inject + AWWalletConnectClient awWalletConnectClient; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_wallet_connect_sessions); toolbar(); setTitle(getString(R.string.title_wallet_connect)); - wallet = getIntent().getParcelableExtra(WALLET); initViewModel(); + recyclerView = findViewById(R.id.list); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.addItemDecoration(new ListDivider(this)); layoutNoActiveSessions = findViewById(R.id.layout_no_sessions); btnConnectWallet = findViewById(R.id.btn_connect_wallet); btnConnectWallet.setOnClickListener(v -> openQrScanner()); @@ -100,6 +104,11 @@ private void initViewModel() .get(WalletConnectViewModel.class); viewModel.serviceReady().observe(this, this::onServiceReady); } + + if (broadcastManager == null) + { + broadcastManager = LocalBroadcastManager.getInstance(getApplicationContext()); + } } private void onServiceReady(Boolean aBoolean) @@ -115,49 +124,60 @@ private void onServiceReady(Boolean aBoolean) } } - @Override - public void onPause() - { - super.onPause(); - stopConnectionCheck(); - } - private void setupList() { wcSessions = viewModel.getSessions(); - if (wcSessions != null) + layoutNoActiveSessions.setVisibility(View.VISIBLE); + if (wcSessions.isEmpty()) { - if (wcSessions.isEmpty()) + layoutNoActiveSessions.setVisibility(View.VISIBLE); + // remove ghosting when all items deleted + if (recyclerView != null) { - layoutNoActiveSessions.setVisibility(View.VISIBLE); - } - else - { - layoutNoActiveSessions.setVisibility(View.GONE); - recyclerView = findViewById(R.id.list); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - adapter = new CustomAdapter(); - recyclerView.setAdapter(adapter); - adapter.notifyDataSetChanged(); + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if (adapter != null) + { + adapter.notifyDataSetChanged(); + } } } + else + { + layoutNoActiveSessions.setVisibility(View.GONE); + recyclerView = findViewById(R.id.list); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + adapter = new CustomAdapter(); + recyclerView.setAdapter(adapter); + adapter.notifyDataSetChanged(); + } + + adapter = new CustomAdapter(); + recyclerView.setAdapter(adapter); } @Override public void onResume() { super.onResume(); - connectionCount = -1; initViewModel(); setupList(); startConnectionCheck(); + + viewModel.track(Analytics.Navigation.WALLET_CONNECT_SESSIONS); + } + + @Override + public void onPause() + { + super.onPause(); + stopConnectionCheck(); } @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_scan_wc, menu); + getMenuInflater().inflate(R.menu.menu_wc_sessions, menu); return super.onCreateOptionsMenu(menu); } @@ -172,18 +192,45 @@ else if (item.getItemId() == R.id.action_scan) { openQrScanner(); } + else if (item.getItemId() == R.id.action_delete) + { + View v = findViewById(R.id.action_delete); + openDeleteMenu(v); + } return super.onOptionsItemSelected(item); } private void openQrScanner() { - Intent intent = new Intent(this, QRScanner.class); - intent.putExtra("wallet", wallet); + Intent intent = new Intent(this, QRScannerActivity.class); intent.putExtra(C.EXTRA_UNIVERSAL_SCAN, true); startActivity(intent); } + private void openDeleteMenu(View v) + { + Timber.d("openDeleteMenu: view: %s", v); + PopupMenu popupMenu = new PopupMenu(this, v); + popupMenu.getMenuInflater().inflate(R.menu.menu_wc_sessions_delete, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(item -> { + if (item.getItemId() == R.id.action_delete_empty) + { + // delete empty + viewModel.removeEmptySessions(this, this::setupList); + return true; + } + else if (item.getItemId() == R.id.action_delete_all) + { + Timber.d("openDeleteMenu: deleteAll: "); + viewModel.removeAllSessions(this, this::setupList); + return true; + } + return false; + }); + popupMenu.show(); + } + private void setupClient(final String sessionId, final CustomAdapter.CustomViewHolder holder) { viewModel.getClient(this, sessionId, client -> handler.post(() -> { @@ -201,16 +248,27 @@ private void setupClient(final String sessionId, final CustomAdapter.CustomViewH private void dialogConfirmDelete(WalletConnectSessionItem session) { - AWalletAlertDialog dialog = new AWalletAlertDialog(this); - dialog.setTitle(R.string.title_delete_session); - dialog.setMessage(getString(R.string.delete_session, session.name)); - dialog.setButton(R.string.delete, v -> { - dialog.dismiss(); - viewModel.deleteSession(session.sessionId); - setupList(); - }); - dialog.setSecondaryButton(R.string.action_cancel, v -> dialog.dismiss()); - dialog.setCancelable(false); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + AlertDialog dialog = builder.setTitle(R.string.title_delete_session) + .setMessage(getString(R.string.delete_session, session.name)) + .setPositiveButton(R.string.delete, (d, w) -> { + viewModel.deleteSession(session, new AWWalletConnectClient.WalletConnectV2Callback() + { + @Override + public void onSessionDisconnected() + { + runOnUiThread(() -> { + setupList(); + awWalletConnectClient.updateNotification(); + }); + } + }); + }) + .setNegativeButton(R.string.action_cancel, (d, w) -> { + d.dismiss(); + }) + .setCancelable(false) + .create(); dialog.show(); } @@ -244,14 +302,19 @@ public void onBindViewHolder(CustomAdapter.CustomViewHolder holder, int position .load(session.icon) .circleCrop() .into(holder.icon); - holder.peerName.setText(session.name); + if (TextUtils.isEmpty(session.name)) + { + holder.peerName.setText(R.string.no_title); + } + else + { + holder.peerName.setText(session.name); + } holder.peerUrl.setText(session.url); holder.chainIcon.setImageResource(EthereumNetworkRepository.getChainLogo(session.chainId)); holder.clickLayer.setOnClickListener(v -> { - //go to wallet connect session page - Intent intent = new Intent(getApplication(), WalletConnectActivity.class); - intent.putExtra("session", session.sessionId); - startActivity(intent); + Context context = getApplicationContext(); + context.startActivity(newIntent(context, session)); }); setupClient(session.sessionId, holder); @@ -292,4 +355,21 @@ class CustomViewHolder extends RecyclerView.ViewHolder } } } + + public static Intent newIntent(Context context, WalletConnectSessionItem session) + { + Intent intent; + if (session instanceof WalletConnectV2SessionItem) + { + intent = new Intent(context, WalletConnectV2Activity.class); + intent.putExtra("session", (WalletConnectV2SessionItem) session); + } + else + { + intent = new Intent(context, WalletConnectActivity.class); + intent.putExtra("session", session.sessionId); + } + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + return intent; + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java b/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java new file mode 100644 index 0000000000..5ac50760c3 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java @@ -0,0 +1,417 @@ +package com.alphawallet.app.ui; + +import static java.util.stream.Collectors.toList; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.NetworkInfo; +import com.alphawallet.app.entity.StandardFunctionInterface; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.walletconnect.NamespaceParser; +import com.alphawallet.app.entity.walletconnect.WalletConnectV2SessionItem; +import com.alphawallet.app.ui.widget.adapter.ChainAdapter; +import com.alphawallet.app.ui.widget.adapter.MethodAdapter; +import com.alphawallet.app.ui.widget.adapter.WalletAdapter; +import com.alphawallet.app.util.LayoutHelper; +import com.alphawallet.app.viewmodel.SelectNetworkFilterViewModel; +import com.alphawallet.app.viewmodel.WalletConnectV2ViewModel; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; +import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.widget.FunctionButtonBar; +import com.bumptech.glide.Glide; +import com.walletconnect.sign.client.Sign; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class WalletConnectV2Activity extends BaseActivity implements StandardFunctionInterface, AWWalletConnectClient.WalletConnectV2Callback +{ + @Inject + AWWalletConnectClient awWalletConnectClient; + private WalletConnectV2ViewModel viewModel; + private SelectNetworkFilterViewModel selectNetworkFilterViewModel; + private ImageView icon; + private TextView peerName; + private TextView peerUrl; + private ProgressBar progressBar; + private LinearLayout infoLayout; + private TextView networksLabel; + private ListView walletList; + private ListView chainList; + private ListView methodList; + private FunctionButtonBar functionBar; + private WalletAdapter walletAdapter; + private WalletConnectV2SessionItem session; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_wallet_connect_v2); + toolbar(); + setTitle(getString(R.string.title_wallet_connect)); + initViews(); + + String url = retrieveUrl(); + if (!TextUtils.isEmpty(url)) + { + progressBar.setVisibility(View.VISIBLE); + awWalletConnectClient.pair(url, (msg) -> { + if (TextUtils.isEmpty(msg)) + { + return; + } + runOnUiThread(() -> { + Toast.makeText(WalletConnectV2Activity.this, msg, Toast.LENGTH_SHORT).show(); + finish(); + }); + }); + return; + } + + this.session = retrieveSession(getIntent()); + initViewModel(); + } + + private void initViews() + { + progressBar = findViewById(R.id.progress); + infoLayout = findViewById(R.id.layout_info); + icon = findViewById(R.id.icon); + peerName = findViewById(R.id.peer_name); + peerUrl = findViewById(R.id.peer_url); + networksLabel = findViewById(R.id.label_networks); + walletList = findViewById(R.id.wallet_list); + chainList = findViewById(R.id.chain_list); + methodList = findViewById(R.id.method_list); + functionBar = findViewById(R.id.layoutButtons); + + progressBar.setVisibility(View.VISIBLE); + infoLayout.setVisibility(View.GONE); + functionBar.setupFunctions(this, Arrays.asList(R.string.dialog_approve, R.string.dialog_reject)); + functionBar.setVisibility(View.GONE); + } + + @Override + protected void onNewIntent(Intent intent) + { + super.onNewIntent(intent); + this.session = retrieveSession(intent); + initViewModel(); + } + + private String retrieveUrl() + { + return getIntent().getStringExtra("url"); + } + + private WalletConnectV2SessionItem retrieveSession(Intent intent) + { + return intent.getParcelableExtra("session"); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(WalletConnectV2ViewModel.class); + selectNetworkFilterViewModel = new ViewModelProvider(this) + .get(SelectNetworkFilterViewModel.class); + viewModel.defaultWallet().observe(this, this::onDefaultWallet); + viewModel.wallets().observe(this, this::onWallets); + } + + private void onWallets(Wallet[] wallets) + { + viewModel.fetchDefaultWallet(); + } + + private void onDefaultWallet(Wallet wallet) + { + if (wallet.type == WalletType.WATCH) + { + AWalletAlertDialog errorDialog = new AWalletAlertDialog(this); + errorDialog.setTitle(R.string.title_dialog_error); + errorDialog.setMessage(getString(R.string.error_message_watch_only_wallet)); + errorDialog.setButton(R.string.dialog_ok, v -> { + errorDialog.dismiss(); + finish(); + }); + errorDialog.show(); + } + else + { + displaySessionStatus(session, wallet); + progressBar.setVisibility(View.GONE); + functionBar.setVisibility(View.VISIBLE); + infoLayout.setVisibility(View.VISIBLE); + } + } + + private void displaySessionStatus(WalletConnectV2SessionItem session, Wallet wallet) + { + if (session.icon == null) + { + icon.setImageResource(R.drawable.grey_circle); + } + else + { + Glide.with(this) + .load(session.icon) + .circleCrop() + .into(icon); + } + + if (!TextUtils.isEmpty(session.name)) + { + peerName.setText(session.name); + } + + peerUrl.setText(session.url); + peerUrl.setTextColor(ContextCompat.getColor(this, R.color.brand)); + peerUrl.setOnClickListener(v -> { + String url = peerUrl.getText().toString(); + if (url.startsWith("http")) + { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + } + }); + + if (session.settled) + { + walletAdapter = new WalletAdapter(this, findWallets(session.wallets)); + networksLabel.setText(R.string.network); + } + else + { + walletAdapter = new WalletAdapter(this, new Wallet[]{wallet}, viewModel.defaultWallet().getValue()); + } + + walletList.setAdapter(walletAdapter); + + if (session.chains.size() > 1) + { + networksLabel.setText(R.string.network); + } + else + { + networksLabel.setText(R.string.subtitle_network); + } + + chainList.setAdapter(new ChainAdapter(this, session.chains)); + + methodList.setAdapter(new MethodAdapter(this, session.methods)); + + resizeList(); + + if (session.settled) + { + setTitle(getString(R.string.title_session_details)); + + functionBar.setupFunctions(new StandardFunctionInterface() + { + @Override + public void handleClick(String action, int actionId) + { + endSessionDialog(); + } + }, Collections.singletonList(R.string.action_end_session)); + } + else + { + setTitle(getString(R.string.title_session_proposal)); + + functionBar.setupFunctions(new StandardFunctionInterface() + { + @Override + public void handleClick(String action, int actionId) + { + if (actionId == R.string.dialog_approve) + { + approve(AWWalletConnectClient.sessionProposal, wallet.address); + } + else + { + reject(AWWalletConnectClient.sessionProposal); + } + } + }, Arrays.asList(R.string.dialog_approve, R.string.dialog_reject)); + } + + } + + private void resizeList() + { + LayoutHelper.resizeList(chainList); + LayoutHelper.resizeList(methodList); + } + + private void endSessionDialog() + { + runOnUiThread(() -> + { + AWalletAlertDialog dialog = new AWalletAlertDialog(this, AWalletAlertDialog.ERROR); + dialog.setTitle(R.string.dialog_title_disconnect_session); + dialog.setButton(R.string.action_close, v -> { + dialog.dismiss(); + killSession(session.sessionId); + }); + dialog.setSecondaryButton(R.string.action_cancel, v -> dialog.dismiss()); + dialog.setCancelable(false); + dialog.show(); + }); + } + + private void killSession(String sessionId) + { + awWalletConnectClient.disconnect(sessionId, this); + } + + private void reject(Sign.Model.SessionProposal sessionProposal) + { + awWalletConnectClient.reject(sessionProposal, this); + } + + private void approve(Sign.Model.SessionProposal sessionProposal, String walletAddress) + { + List disabledNetworks = disabledNetworks(sessionProposal.getRequiredNamespaces()); + if (disabledNetworks.isEmpty()) + { + awWalletConnectClient.approve(sessionProposal, getSelectedAccounts(), this); + } + else + { + showDialog(disabledNetworks); + } + } + + private void showDialog(List disabledNetworks) + { + AWalletAlertDialog dialog = new AWalletAlertDialog(this); + dialog.setMessage(String.format(getString(R.string.network_must_be_enabled), joinNames(disabledNetworks))); + dialog.setButton(R.string.select_active_networks, view -> { + Intent intent = new Intent(this, SelectNetworkFilterActivity.class); + startActivity(intent); + dialog.dismiss(); + }); + dialog.setSecondaryButton(R.string.action_cancel, (view) -> dialog.dismiss()); + dialog.show(); + } + + @NonNull + private String joinNames(List disabledNetworks) + { + return disabledNetworks.stream() + .map((chainId) -> { + NetworkInfo network = selectNetworkFilterViewModel.getNetworkByChain(chainId); + if (network != null) + { + return network.name; + } + return String.valueOf(chainId); + }) + .collect(Collectors.joining(", ")); + } + + private List disabledNetworks(Map requiredNamespaces) + { + NamespaceParser namespaceParser = new NamespaceParser(); + namespaceParser.parseProposal(requiredNamespaces); + List enabledChainIds = selectNetworkFilterViewModel.getActiveNetworks(); + List result = new ArrayList<>(); + List chains = namespaceParser.getChains().stream().map((s) -> Long.parseLong(s.split(":")[1])).collect(toList()); + for (Long chainId : chains) + { + if (!enabledChainIds.contains(chainId)) + { + result.add(chainId); + } + } + return result; + } + + private List findWallets(List addresses) + { + List result = new ArrayList<>(); + if (viewModel.wallets().getValue() == null) + { + return result; + } + + Map map = toMap(Objects.requireNonNull(viewModel.wallets().getValue())); + for (String address : addresses) + { + Wallet wallet = map.get(address); + if (wallet == null) + { + wallet = new Wallet(address); + } + result.add(wallet); + } + return result; + } + + private Map toMap(Wallet[] wallets) + { + HashMap map = new HashMap<>(); + for (Wallet wallet : wallets) + { + map.put(wallet.address, wallet); + } + return map; + } + + private List getSelectedAccounts() + { + return walletAdapter.getSelectedWallets().stream() + .map((wallet) -> wallet.address).collect(toList()); + } + + @Override + public void onSessionProposalApproved() + { + finish(); + } + + @Override + public void onSessionProposalRejected() + { + finish(); + } + + @Override + public void onSessionDisconnected() + { + finish(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletDiagnosticActivity.java b/app/src/main/java/com/alphawallet/app/ui/WalletDiagnosticActivity.java new file mode 100644 index 0000000000..511ee1c784 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/WalletDiagnosticActivity.java @@ -0,0 +1,477 @@ +package com.alphawallet.app.ui; + +import static com.alphawallet.app.service.KeystoreAccountService.KEYSTORE_FOLDER; +import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; +import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Pair; +import android.view.MenuItem; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.AuthenticationCallback; +import com.alphawallet.app.entity.AuthenticationFailType; +import com.alphawallet.app.entity.Operation; +import com.alphawallet.app.entity.StandardFunctionInterface; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.service.KeyService; +import com.alphawallet.app.service.KeystoreAccountService; +import com.alphawallet.app.viewmodel.BackupKeyViewModel; +import com.alphawallet.app.widget.AWalletAlertDialog; +import com.alphawallet.app.widget.FunctionButtonBar; +import com.alphawallet.app.widget.SignTransactionDialog; +import com.alphawallet.token.tools.Numeric; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.WalletFile; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import dagger.hilt.android.AndroidEntryPoint; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import wallet.core.jni.CoinType; +import wallet.core.jni.HDWallet; +import wallet.core.jni.PrivateKey; + +/** + * Created by JB on 24/08/2022. + * + * NB: do not use any of this code anywhere else in the wallet! This is purely for key diagnostics to help support diagnose issues with keys + */ +@AndroidEntryPoint +public class WalletDiagnosticActivity extends BaseActivity implements StandardFunctionInterface +{ + private BackupKeyViewModel viewModel; + private AWalletAlertDialog dialog; + + private Wallet wallet; + + private boolean isLegacyKeystore = false; + private boolean isKeyStore = false; + private boolean isSeedPhrase = false; + private boolean isLocked = false; + + private final Handler handler = new Handler(Looper.getMainLooper()); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_wallet_diagnostic); + toolbar(); + setTitle(getString(R.string.key_diagnostic)); + + FunctionButtonBar functionBar = findViewById(R.id.layoutButtons); + functionBar.setupFunctions(this, new ArrayList<>(Collections.singletonList(R.string.run_key_diagnostic))); + functionBar.revealButtons(); + + if (getIntent() != null) + { + wallet = (Wallet) getIntent().getExtras().get("wallet"); + } + else + { + finish(); + } + + initViewModel(); + startKeyDiagnostic(); + } + + @Override + public void handleClick(String action, int actionId) + { + //test cipher + doUnlock(new UnlockCallback() + { + @Override + public void carryOn(boolean passed) + { + Pair res = viewModel.testCipher(wallet.address, KeyService.LEGACY_CIPHER_ALGORITHM); + switch (res.first) + { + case UNKNOWN: + case REQUIRES_AUTH: + showError("Unknown Failure"); + break; + case INVALID_CIPHER: + isLegacyKeystore = false; + break; + case SUCCESSFUL_DECODE: + isLegacyKeystore = true; + evaluateKey(); + break; + case IV_NOT_FOUND: + showError("IV File not found"); + return; + case ENCRYPTED_FILE_NOT_FOUND: + showError("Encrypted Data File not found"); + return; + } + + testKeyStore(); + } + }); + } + + private void startKeyDiagnostic() + { + LinearLayout successOverlay = findViewById(R.id.layout_success_overlay); + if (successOverlay != null) successOverlay.setVisibility(View.GONE); + + setCurrentKeyType(); + boolean hasKey = scanForKey(); + + if (hasKey) + { + unlockKeyIfRequired(); + } + else + { + showError("Unable to find enclave key for this wallet. Is it a watch wallet?"); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == android.R.id.home) + { + onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider(this) + .get(BackupKeyViewModel.class); + } + + private boolean scanForKey() + { + TextView status = findViewById(R.id.key_in_enclave); + if (viewModel.hasKey(wallet.address)) + { + status.setText(R.string.key_found); + status.setTextColor(getColor(R.color.green)); + return true; + } + else + { + status.setText(R.string.key_not_found); + status.setTextColor(getColor(R.color.danger)); + return false; + } + } + + private void setCurrentKeyType() + { + TextView status = findViewById(R.id.key_type); + String walletType = wallet.type.toString(); + status.setText(walletType); + } + + private void unlockKeyIfRequired() + { + TextView lockedState = findViewById(R.id.key_is_locked); + //first test key to see if it's unlocked + if (!isLocked) + { + Pair res = viewModel.testCipher(wallet.address, KeyService.LEGACY_CIPHER_ALGORITHM); + + isLocked = (res.first == KeyService.KeyExceptionType.REQUIRES_AUTH); + lockedState.setText(isLocked ? "Locked" : "Unlocked"); + lockedState.setTextColor(isLocked ? getColor(R.color.green) : getColor(R.color.danger)); + } + } + + // Finally, test if key matches up with what's stored in the database + private void evaluateKey() + { + WalletType actualType = getActualKeyType(); + if (actualType == WalletType.NOT_DEFINED) return; + + switch (wallet.type) + { + case WATCH: + case NOT_DEFINED: + case TEXT_MARKER: + case LARGE_TITLE: + break; + case KEYSTORE: + if (!isKeyStore) + { + suggestCorrectWallet("Database says Keystore but tests show: " + actualType.toString(), actualType); + } + else + { + showSuccess(); + } + break; + case HDKEY: + if (!isSeedPhrase) + { + suggestCorrectWallet("Database says Seed Phrase but tests show: " + actualType.toString(), actualType); + } + else + { + showSuccess(); + } + break; + case KEYSTORE_LEGACY: + if (!isLegacyKeystore) + { + suggestCorrectWallet("Database says Keystore Legacy but tests show: " + actualType.toString(), actualType); + } + else + { + showSuccess(); + } + break; + } + } + + private WalletType getActualKeyType() + { + if (isLegacyKeystore) + { + return WalletType.KEYSTORE_LEGACY; + } + else if (isSeedPhrase) + { + return WalletType.HDKEY; + } + else if (isKeyStore) + { + return WalletType.KEYSTORE; + } + else + { + return WalletType.NOT_DEFINED; + } + } + + private void suggestCorrectWallet(String suggest, WalletType type) + { + if (dialog != null && dialog.isShowing()) dialog.dismiss(); + dialog = new AWalletAlertDialog(this); + dialog.setTitle(R.string.key_status); + dialog.setMessage("Database keytype mismatch. " + suggest); + dialog.setIcon(WARNING); + dialog.setButtonText(R.string.fix_key_state); + dialog.setButtonListener(v -> { + updateKeyState(type); + }); + dialog.setSecondaryButtonText(R.string.action_cancel); + dialog.setSecondaryButtonListener(v -> { + dialog.dismiss(); + }); + dialog.show(); + } + + private void updateKeyState(WalletType type) + { + wallet.type = type; + viewModel.storeWallet(wallet) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(this::completeUpdate, error -> showError(error.getMessage())) + .isDisposed(); + } + + private void completeUpdate(Wallet wallet) + { + LinearLayout successOverlay = findViewById(R.id.layout_success_overlay); + if (successOverlay != null) successOverlay.setVisibility(View.VISIBLE); + //restart key scan + handler.postDelayed(this::startKeyDiagnostic, 1000); + } + + private void showSuccess() + { + final LinearLayout successOverlay = findViewById(R.id.layout_success_overlay); + if (successOverlay != null) successOverlay.setVisibility(View.VISIBLE); + //restart key scan + handler.postDelayed(() -> { + if (successOverlay != null) successOverlay.setVisibility(View.GONE); + }, 1000); + } + + //Now test if this is a v2 keystore + private void testKeyStore() + { + Pair res = viewModel.testCipher(wallet.address, KeyService.CIPHER_ALGORITHM); + switch (res.first) + { + case UNKNOWN: + case REQUIRES_AUTH: + showError("Unknown Failure"); + break; + case INVALID_CIPHER: + isKeyStore = false; + showError("Key Failure: Invalid Cipher"); + break; + case SUCCESSFUL_DECODE: + //may or may not be a keystore, could be a seed phrase + boolean testKey = testKeyType(res.second); + if (testKey) evaluateKey(); + break; + case IV_NOT_FOUND: + showError("IV File not found"); + return; + case ENCRYPTED_FILE_NOT_FOUND: + showError("Encrypted Data File not found"); + return; + } + } + + private boolean testKeyType(String keyData) + { + //could either be a seed phrase or a keystore + Pattern pattern = Pattern.compile(ImportSeedFragment.validator, Pattern.MULTILINE); + TextView status = findViewById(R.id.status_txt); + + //first check for seed phrase + final Matcher matcher = pattern.matcher(keyData); + if (!matcher.find()) + { + int wordCount = wordCount(keyData); + + if (wordCount == 12 || wordCount == 18 || wordCount == 24) + { + //is valid seed phrase + HDWallet newWallet = new HDWallet(keyData, ""); + PrivateKey pk = newWallet.getKeyForCoin(CoinType.ETHEREUM); + + status.setText(getString(R.string.seed_phrase_public_key, Numeric.toHexString(pk.getPublicKeySecp256k1(false).data()))); + status.setTextColor(getColor(R.color.green)); + isSeedPhrase = true; + return true; + } + } + + if (!isSeedPhrase) + { + //attempt to recover the key + File keyFolder = new File(getFilesDir(), KEYSTORE_FOLDER); + try + { + Credentials credentials = KeystoreAccountService.getCredentialsWithThrow(keyFolder, wallet.address, keyData); + + if (credentials == null) + { + showError("Unable to find Keystore File"); + } + else + { + status.setText(getString(R.string.keystore_public_key, credentials.getEcKeyPair().getPublicKey().toString(16))); + isKeyStore = true; + return true; + } + } + catch (Exception e) + { + showError("Keystore decode error: " + e.getMessage()); + } + } + + return false; + } + + private int wordCount(String value) + { + if (value == null || value.length() == 0) return 0; + String[] split = value.split("\\s+"); + return split.length; + } + + // DO NOT use this style of key authentication in any other code. It's here like this only for key diagnostics + // Always use the ActionSheet + implement ActionSheetCallback as per SendActivity, NFTAssetDetailActivity etc + private void doUnlock(UnlockCallback cb) + { + SignTransactionDialog unlockTx = new SignTransactionDialog(this); + unlockTx.getAuthentication(new AuthenticationCallback() + { + @Override + public void authenticatePass(Operation callbackId) + { + cb.carryOn(true); + } + + @Override + public void authenticateFail(String fail, AuthenticationFailType failType, Operation callbackId) + { + cb.carryOn(false); + } + + @Override + public void legacyAuthRequired(Operation callbackId, String dialogTitle, String desc) + { + // not interested + } + }, this, Operation.FETCH_MNEMONIC); + } + + //This function could be useful for future, in case this is needed + @SuppressWarnings("unused") + private String dumpKeystoreFromSeedPhrase(String seedPhrase, String keystorePassword) + { + HDWallet newWallet = new HDWallet(seedPhrase, ""); + PrivateKey pk = newWallet.getKeyForCoin(CoinType.ETHEREUM); + ECKeyPair keyPair = ECKeyPair.create(pk.data()); + + try + { + WalletFile wf = org.web3j.crypto.Wallet.createLight(keystorePassword, keyPair); + return objectMapper.writeValueAsString(wf); + } + catch (Exception e) + { + e.printStackTrace(); + } + + return ""; + } + + private void showError(String error) + { + TextView statusTxt = findViewById(R.id.status_txt); + statusTxt.setText(error); + statusTxt.setTextColor(getColor(R.color.danger)); + if (dialog != null && dialog.isShowing()) dialog.dismiss(); + dialog = new AWalletAlertDialog(this); + dialog.setTitle(R.string.title_dialog_error); + dialog.setMessage(error); + dialog.setIcon(ERROR); + dialog.setButtonText(R.string.button_ok); + dialog.setButtonListener(v -> { + dialog.dismiss(); + }); + dialog.show(); + } + + private interface UnlockCallback + { + default void carryOn(boolean passed) { }; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java index 2fff854596..c161abfa89 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java @@ -39,15 +39,14 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.BackupOperationType; -import com.alphawallet.app.entity.BackupTokenCallback; import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.ServiceSyncCallback; import com.alphawallet.app.entity.TokenFilter; import com.alphawallet.app.entity.Wallet; -import com.alphawallet.app.entity.WalletPage; import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; @@ -65,23 +64,30 @@ import com.alphawallet.app.ui.widget.holder.WarningHolder; import com.alphawallet.app.util.LocaleUtils; import com.alphawallet.app.viewmodel.WalletViewModel; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; +import com.alphawallet.app.widget.BuyEthOptionsView; import com.alphawallet.app.widget.LargeTitleView; import com.alphawallet.app.widget.NotificationView; import com.alphawallet.app.widget.ProgressView; import com.alphawallet.app.widget.SystemView; import com.alphawallet.app.widget.UserAvatar; +import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; import io.realm.RealmResults; +import timber.log.Timber; /** * Created by justindeguzman on 2/28/18. @@ -91,21 +97,17 @@ public class WalletFragment extends BaseFragment implements TokensAdapterCallback, View.OnClickListener, Runnable, - BackupTokenCallback, AvatarWriteCallback, ServiceSyncCallback { - private static final String TAG = "WFRAG"; - public static final String SEARCH_FRAGMENT = "w_search"; - + private static final String TAG = "WFRAG"; + private final Handler handler = new Handler(Looper.getMainLooper()); private WalletViewModel viewModel; - private SystemView systemView; private TokensAdapter adapter; private UserAvatar addressAvatar; private View selectedToken; - private final Handler handler = new Handler(Looper.getMainLooper()); private String importFileName; private RecyclerView recyclerView; private SwipeRefreshLayout refreshLayout; @@ -115,6 +117,11 @@ public class WalletFragment extends BaseFragment implements private RealmResults realmUpdates; private LargeTitleView largeTitleView; private long realmUpdateTime; + private ActivityResultLauncher handleBackupClick; + private ActivityResultLauncher tokenManagementLauncher; + + @Inject + AWWalletConnectClient awWalletConnectClient; private ActivityResultLauncher networkSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> @@ -140,6 +147,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c toolbar(view); } + initResultLaunchers(); + initViews(view); initViewModel(); @@ -172,6 +181,44 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c return view; } + private void initResultLaunchers() + { + tokenManagementLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + if (result.getData() == null) return; + ArrayList tokenData = result.getData().getParcelableArrayListExtra(ADDED_TOKEN); + Bundle b = new Bundle(); + b.putParcelableArrayList(C.ADDED_TOKEN, tokenData); + getParentFragmentManager().setFragmentResult(C.ADDED_TOKEN, b); + }); + + networkSettingsHandler = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + //send instruction to restart tokenService + getParentFragmentManager().setFragmentResult(RESET_TOKEN_SERVICE, new Bundle()); + }); + + handleBackupClick = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> + { + String keyBackup = null; + boolean noLockScreen = false; + Intent data = result.getData(); + if (data != null) keyBackup = data.getStringExtra("Key"); + if (data != null) noLockScreen = data.getBooleanExtra("nolock", false); + if (result.getResultCode() == RESULT_OK) + { + ((HomeActivity) getActivity()).backupWalletSuccess(keyBackup); + } + else + { + ((HomeActivity) getActivity()).backupWalletFail(keyBackup, noLockScreen); + } + }); + } + private void initList() { adapter = new TokensAdapter(this, viewModel.getAssetDefinitionService(), viewModel.getTokensService(), @@ -200,6 +247,9 @@ private void initViewModel() viewModel.defaultWallet().observe(getViewLifecycleOwner(), this::onDefaultWallet); viewModel.onFiatValues().observe(getViewLifecycleOwner(), this::updateValue); viewModel.getTokensService().startWalletSync(this); + viewModel.activeWalletConnectSessions().observe(getViewLifecycleOwner(), walletConnectSessionItems -> { + adapter.showActiveWalletConnectSessions(walletConnectSessionItems); + }); } private void initViews(@NonNull View view) @@ -294,8 +344,9 @@ private void updateMetas(List metas) { if (metas.size() > 0) { - adapter.setTokens(metas.toArray(new TokenCardMeta[0])); + adapter.updateTokenMetas(metas.toArray(new TokenCardMeta[0])); systemView.hide(); + viewModel.checkDeleteMetas(metas); } }); } @@ -356,6 +407,7 @@ private void refreshList() adapter.clear(); viewModel.prepare(); viewModel.notifyRefresh(); + awWalletConnectClient.updateNotification(); }); } @@ -493,8 +545,19 @@ public void reloadTokens() @Override public void onBuyToken() { - Intent intent = viewModel.getBuyIntent(getCurrentWallet().address); - ((HomeActivity) getActivity()).onActivityResult(C.TOKEN_SEND_ACTIVITY, RESULT_OK, intent); + BottomSheetDialog buyEthDialog = new BottomSheetDialog(getActivity()); + BuyEthOptionsView buyEthOptionsView = new BuyEthOptionsView(getActivity()); + buyEthOptionsView.setOnBuyWithRampListener(v -> { + Intent intent = viewModel.getBuyIntent(getCurrentWallet().address); + ((HomeActivity) getActivity()).onActivityResult(C.TOKEN_SEND_ACTIVITY, RESULT_OK, intent); + viewModel.track(Analytics.Action.BUY_WITH_RAMP); + buyEthDialog.dismiss(); + }); + buyEthOptionsView.setOnBuyWithCoinbasePayListener(v -> { + viewModel.showBuyEthOptions(getActivity()); + }); + buyEthDialog.setContentView(buyEthOptionsView); + buyEthDialog.show(); } @Override @@ -505,11 +568,15 @@ public void onResume() selectedToken = null; if (viewModel == null) { - ((HomeActivity) getActivity()).resetFragment(WalletPage.WALLET); + requireActivity().recreate(); } - else if (largeTitleView != null) + else { - largeTitleView.setVisibility(viewModel.getTokensService().isMainNetActive() ? View.VISIBLE : View.GONE); //show or hide Fiat summary + viewModel.track(Analytics.Navigation.WALLET); + if (largeTitleView != null) + { + largeTitleView.setVisibility(viewModel.getTokensService().isMainNetActive() ? View.VISIBLE : View.GONE); //show or hide Fiat summary + } } } @@ -533,6 +600,15 @@ private void onTokens(TokenCardMeta[] tokens) { setRealmListener(realmUpdateTime); } + + if (currentTabPos.equals(TokenFilter.ALL)) + { + awWalletConnectClient.updateNotification(); + } + else + { + adapter.showActiveWalletConnectSessions(Collections.emptyList()); + } } /** @@ -621,12 +697,20 @@ public void onDestroy() handler.removeCallbacksAndMessages(null); if (realmUpdates != null) { - realmUpdates.removeAllChangeListeners(); + try + { + realmUpdates.removeAllChangeListeners(); + } + catch (Exception e) + { + Timber.e(e); + } } if (realm != null && !realm.isClosed()) realm.close(); if (adapter != null && recyclerView != null) adapter.onDestroy(recyclerView); } + @Override public void resetTokens() { if (viewModel != null && adapter != null) @@ -654,24 +738,6 @@ public void run() selectedToken = null; } - ActivityResultLauncher handleBackupClick = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - String keyBackup = null; - boolean noLockScreen = false; - Intent data = result.getData(); - if (data != null) keyBackup = data.getStringExtra("Key"); - if (data != null) noLockScreen = data.getBooleanExtra("nolock", false); - if (result.getResultCode() == RESULT_OK) - { - ((HomeActivity) getActivity()).backupWalletSuccess(keyBackup); - } - else - { - ((HomeActivity) getActivity()).backupWalletFail(keyBackup, noLockScreen); - } - }); - @Override public void backUpClick(Wallet wallet) { @@ -698,29 +764,21 @@ public void remindMeLater(Wallet wallet) handler.post(() -> { if (viewModel != null) viewModel.setKeyWarningDismissTime(wallet.address); - if (adapter != null) adapter.removeBackupWarning(); + if (adapter != null) adapter.removeItem(WarningHolder.VIEW_TYPE); }); } - final ActivityResultLauncher tokenManagementLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> - { - if (result.getData() == null) return; - ArrayList tokenData = result.getData().getParcelableArrayListExtra(ADDED_TOKEN); - Bundle b = new Bundle(); - b.putParcelableArrayList(C.ADDED_TOKEN, tokenData); - getParentFragmentManager().setFragmentResult(C.ADDED_TOKEN, b); - }); - + @Override public void storeWalletBackupTime(String backedUpKey) { handler.post(() -> { if (viewModel != null) viewModel.setKeyBackupTime(backedUpKey); - if (adapter != null) adapter.removeBackupWarning(); + if (adapter != null) adapter.removeItem(WarningHolder.VIEW_TYPE); }); } + @Override public void setImportFilename(String fName) { importFileName = fName; @@ -733,6 +791,62 @@ public void avatarFound(Wallet wallet) viewModel.saveAvatar(wallet); } + public Wallet getCurrentWallet() + { + return viewModel.getWallet(); + } + + @Override + public boolean onMenuItemClick(MenuItem menuItem) + { + if (menuItem.getItemId() == R.id.action_my_wallet) + { + viewModel.showMyAddress(requireActivity()); + } + if (menuItem.getItemId() == R.id.action_scan) + { + viewModel.showQRCodeScanning(getActivity()); + } + return super.onMenuItemClick(menuItem); + } + + private void initNotificationView(View view) + { + NotificationView notificationView = view.findViewById(R.id.notification); + boolean hasShownWarning = viewModel.isMarshMallowWarningShown(); + + if (!hasShownWarning && android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) + { + notificationView.setTitle(getContext().getString(R.string.title_version_support_warning)); + notificationView.setMessage(getContext().getString(R.string.message_version_support_warning)); + notificationView.setPrimaryButtonText(getContext().getString(R.string.hide_notification)); + notificationView.setPrimaryButtonListener(() -> + { + notificationView.setVisibility(View.GONE); + viewModel.setMarshMallowWarning(true); + }); + } + else + { + notificationView.setVisibility(View.GONE); + } + } + + @Override + public void onSearchClicked() + { + Intent intent = new Intent(getActivity(), SearchActivity.class); + startActivity(intent); + } + + @Override + public void onSwitchClicked() + { + Intent intent = new Intent(getActivity(), SelectNetworkFilterActivity.class); + intent.putExtra(C.EXTRA_SINGLE_ITEM, false); + networkSettingsHandler.launch(intent); + } + public class SwipeCallback extends ItemTouchHelper.SimpleCallback { private final TokensAdapter mAdapter; @@ -841,60 +955,4 @@ else if (dX < 0) icon.draw(c); } } - - public Wallet getCurrentWallet() - { - return viewModel.getWallet(); - } - - @Override - public boolean onMenuItemClick(MenuItem menuItem) - { - if (menuItem.getItemId() == R.id.action_my_wallet) - { - viewModel.showMyAddress(getContext()); - } - if (menuItem.getItemId() == R.id.action_scan) - { - viewModel.showQRCodeScanning(getActivity()); - } - return super.onMenuItemClick(menuItem); - } - - private void initNotificationView(View view) - { - NotificationView notificationView = view.findViewById(R.id.notification); - boolean hasShownWarning = viewModel.isMarshMallowWarningShown(); - - if (!hasShownWarning && android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) - { - notificationView.setTitle(getContext().getString(R.string.title_version_support_warning)); - notificationView.setMessage(getContext().getString(R.string.message_version_support_warning)); - notificationView.setPrimaryButtonText(getContext().getString(R.string.hide_notification)); - notificationView.setPrimaryButtonListener(() -> - { - notificationView.setVisibility(View.GONE); - viewModel.setMarshMallowWarning(true); - }); - } - else - { - notificationView.setVisibility(View.GONE); - } - } - - @Override - public void onSearchClicked() - { - Intent intent = new Intent(getActivity(), SearchActivity.class); - startActivity(intent); - } - - @Override - public void onSwitchClicked() - { - Intent intent = new Intent(getActivity(), SelectNetworkFilterActivity.class); - intent.putExtra(C.EXTRA_SINGLE_ITEM, false); - networkSettingsHandler.launch(intent); - } } diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletsActivity.java b/app/src/main/java/com/alphawallet/app/ui/WalletsActivity.java index d8e0d3c8a1..375243573f 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletsActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletsActivity.java @@ -27,6 +27,7 @@ import com.alphawallet.app.entity.SyncCallback; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletConnectActions; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.service.KeyService; @@ -40,6 +41,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.snackbar.Snackbar; +import java.util.Map; + import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; @@ -56,9 +59,8 @@ public class WalletsActivity extends BaseActivity implements { private final Handler handler = new Handler(); private final long balanceChain = EthereumNetworkRepository.getOverrideToken().chainId; - WalletsViewModel viewModel; + private WalletsViewModel viewModel; private RecyclerView list; - private SwipeRefreshLayout refreshLayout; private SystemView systemView; private Dialog dialog; private AWalletAlertDialog aDialog; @@ -101,7 +103,15 @@ protected void onResume() { super.onResume(); initViewModel(); - initViews(); + } + + private void scrollToDefaultWallet() + { + int position = adapter.getDefaultWalletIndex(); + if (position != -1) + { + list.getLayoutManager().scrollToPosition(position); + } } private void initViewModel() @@ -118,9 +128,15 @@ private void initViewModel() viewModel.createdWallet().observe(this, this::onCreatedWallet); viewModel.createWalletError().observe(this, this::onCreateWalletError); viewModel.noWalletsError().observe(this, this::noWallets); + viewModel.baseTokens().observe(this, this::updateBaseTokens); } + viewModel.onPrepare(balanceChain, this); + initViews(); //adjust here to change which chain the wallet show the balance of, eg use CLASSIC_ID for an Eth Classic wallet + } - viewModel.onPrepare(balanceChain, this); //adjust here to change which chain the wallet show the balance of, eg use CLASSIC_ID for an Eth Classic wallet + private void updateBaseTokens(Map walletTokens) + { + adapter.setTokens(walletTokens); } protected Activity getThisActivity() @@ -137,7 +153,7 @@ private void noWallets(Boolean aBoolean) private void initViews() { - refreshLayout = findViewById(R.id.refresh_layout); + SwipeRefreshLayout refreshLayout = findViewById(R.id.refresh_layout); list = findViewById(R.id.list); list.setLayoutManager(new LinearLayoutManager(this)); @@ -163,25 +179,19 @@ private void onCreateWalletError(ErrorEnvelope errorEnvelope) @Override public void syncUpdate(String wallet, Pair value) { - runOnUiThread(() -> { - adapter.updateWalletState(wallet, value); - }); + runOnUiThread(() -> adapter.updateWalletState(wallet, value)); } @Override public void syncCompleted(String wallet, Pair value) { - runOnUiThread(() -> { - adapter.completeWalletSync(wallet, value); - }); + runOnUiThread(() -> adapter.completeWalletSync(wallet, value)); } @Override public void syncStarted(String wallet, Pair value) { - runOnUiThread(() -> { - adapter.setUnsyncedWalletValue(wallet, value); - }); + runOnUiThread(() -> adapter.setUnsyncedWalletValue(wallet, value)); } @Override @@ -272,7 +282,7 @@ else if (requestCode == C.IMPORT_REQUEST_CODE) if (importedWallet != null) { requiresHomeRefresh = true; - viewModel.setDefaultWallet(importedWallet); + viewModel.setDefaultWallet(importedWallet, true); } } } @@ -337,6 +347,7 @@ private void onChangeDefaultWallet(Wallet wallet) } adapter.setDefaultWallet(wallet); + scrollToDefaultWallet(); if (requiresHomeRefresh) { viewModel.stopUpdates(); @@ -355,14 +366,19 @@ private void onChangeDefaultWallet(Wallet wallet) private void onFetchWallets(Wallet[] wallets) { enableDisplayHomeAsUp(); - if (adapter != null) adapter.setWallets(wallets); + if (adapter != null) + { + adapter.setWallets(wallets); + scrollToDefaultWallet(); + } + invalidateOptionsMenu(); } private void onCreatedWallet(Wallet wallet) { hideToolbar(); - viewModel.setDefaultWallet(wallet); + viewModel.setDefaultWallet(wallet, true); callNewWalletPage(wallet); finish(); } @@ -386,7 +402,7 @@ private void onError(ErrorEnvelope errorEnvelope) private void onSetWalletDefault(Wallet wallet) { requiresHomeRefresh = true; - viewModel.setDefaultWallet(wallet); + viewModel.setDefaultWallet(wallet, false); } private void hideDialog() diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java new file mode 100644 index 0000000000..c78220dfd9 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java @@ -0,0 +1,52 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.alphawallet.app.R; +import com.alphawallet.app.repository.EthereumNetworkBase; +import com.alphawallet.app.repository.EthereumNetworkRepository; +import com.alphawallet.app.walletconnect.util.WalletConnectHelper; + +import java.util.List; + +public class ChainAdapter extends ArrayAdapter +{ + public ChainAdapter(Context context, List chains) + { + super(context, 0, chains); + } + + @Override + public boolean isEnabled(int position) + { + return false; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) + { + long chainId = WalletConnectHelper.getChainId(getItem(position)); + if (convertView == null) + { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_chain, parent, false); + } + + TextView chainName = convertView.findViewById(R.id.chain_name); + ImageView chainIcon = convertView.findViewById(R.id.chain_icon); + + chainName.setText(EthereumNetworkBase.getNetworkInfo(chainId).name); + chainIcon.setImageResource(EthereumNetworkRepository.getChainLogo(chainId)); + + return convertView; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainFilter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainFilter.java new file mode 100644 index 0000000000..e6af0be5d3 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainFilter.java @@ -0,0 +1,30 @@ +package com.alphawallet.app.ui.widget.adapter; + +import com.alphawallet.app.entity.lifi.Chain; +import com.alphawallet.ethereum.EthereumNetworkBase; + +import java.util.ArrayList; +import java.util.List; + +public class ChainFilter +{ + private final List chains; + + public ChainFilter(List chains) + { + this.chains = chains; + } + + public List getSupportedChains() + { + List filteredChains = new ArrayList<>(); + for (Chain c : chains) + { + if (EthereumNetworkBase.getNetworkByChain(c.id) != null) + { + filteredChains.add(c); + } + } + return filteredChains; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MethodAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MethodAdapter.java new file mode 100644 index 0000000000..eb18499eb4 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MethodAdapter.java @@ -0,0 +1,44 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.alphawallet.app.R; + +import java.util.List; + +public class MethodAdapter extends ArrayAdapter +{ + public MethodAdapter(@NonNull Context context, List methods) + { + super(context, 0, methods); + } + + @Override + public boolean isEnabled(int position) + { + return false; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) + { + if (convertView == null) + { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_method, parent, false); + } + + TextView methodName = convertView.findViewById(R.id.text); + methodName.setText(getItem(position)); + + return convertView; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MultiSelectNetworkAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MultiSelectNetworkAdapter.java index 2934e49374..c0528e1564 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MultiSelectNetworkAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/MultiSelectNetworkAdapter.java @@ -9,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.alphawallet.app.R; +import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.ui.widget.entity.NetworkItem; import com.alphawallet.app.widget.TokenIcon; import com.google.android.material.checkbox.MaterialCheckBox; @@ -18,21 +19,16 @@ import java.util.ArrayList; import java.util.List; -public class MultiSelectNetworkAdapter extends RecyclerView.Adapter { +public class MultiSelectNetworkAdapter extends RecyclerView.Adapter +{ private final List networkList; + private final Callback callback; private boolean hasClicked = false; - public interface EditNetworkListener { - void onEditNetwork(long chainId, View parent); - } - - private final EditNetworkListener editListener; - - - public MultiSelectNetworkAdapter(List selectedNetworks, EditNetworkListener editNetworkListener) + public MultiSelectNetworkAdapter(List selectedNetworks, Callback callback) { networkList = selectedNetworks; - editListener = editNetworkListener; + this.callback = callback; } public Long[] getSelectedItems() @@ -59,7 +55,7 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) View itemView = LayoutInflater.from(parent.getContext()) .inflate(buttonTypeId, parent, false); - return new MultiSelectNetworkAdapter.ViewHolder(itemView); + return new ViewHolder(itemView); } @Override @@ -73,17 +69,31 @@ public void onBindViewHolder(MultiSelectNetworkAdapter.ViewHolder holder, int po holder.chainId.setText(holder.itemLayout.getContext().getString(R.string.chain_id, item.getChainId())); holder.itemLayout.setOnClickListener(v -> clickListener(holder, position)); holder.manageView.setVisibility(View.VISIBLE); - holder.manageView.setOnClickListener(v -> editListener.onEditNetwork(networkList.get(position).getChainId(), holder.manageView)); + holder.manageView.setOnClickListener(v -> callback.onEditSelected(networkList.get(position).getChainId(), holder.manageView)); holder.checkbox.setChecked(item.isSelected()); holder.tokenIcon.bindData(item.getChainId()); + + if (EthereumNetworkBase.isNetworkDeprecated(item.getChainId())) + { + holder.deprecatedIndicator.setVisibility(View.VISIBLE); + holder.tokenIcon.setGrayscale(true); + holder.name.setAlpha(0.7f); + holder.chainId.setAlpha(0.7f); + } } } + public int getSelectedItemCount() + { + return getSelectedItems().length; + } + private void clickListener(final MultiSelectNetworkAdapter.ViewHolder holder, final int position) { networkList.get(position).setSelected(!networkList.get(position).isSelected()); holder.checkbox.setChecked(networkList.get(position).isSelected()); hasClicked = true; + callback.onCheckChanged(networkList.get(position).getChainId(), getSelectedItemCount()); } @Override @@ -92,13 +102,22 @@ public int getItemCount() return networkList.size(); } - class ViewHolder extends RecyclerView.ViewHolder { + public interface Callback + { + void onEditSelected(long chainId, View parent); + + void onCheckChanged(long chainId, int count); + } + + static class ViewHolder extends RecyclerView.ViewHolder + { MaterialCheckBox checkbox; TextView name; View itemLayout; View manageView; TokenIcon tokenIcon; TextView chainId; + TextView deprecatedIndicator; ViewHolder(View view) { @@ -109,6 +128,7 @@ class ViewHolder extends RecyclerView.ViewHolder { manageView = view.findViewById(R.id.manage_btn); tokenIcon = view.findViewById(R.id.token_icon); chainId = view.findViewById(R.id.chain_id); + deprecatedIndicator = view.findViewById(R.id.deprecated); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java index 0b89d2d424..88695efe11 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java @@ -17,6 +17,8 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.repository.EthereumNetworkBase; +import com.alphawallet.app.service.OpenSeaService; import com.alphawallet.app.ui.NFTActivity; import com.alphawallet.app.ui.widget.OnAssetClickListener; import com.alphawallet.app.widget.NFTImageView; @@ -38,16 +40,18 @@ public class NFTAssetsAdapter extends RecyclerView.Adapter> actualData; private final List> displayData; - public NFTAssetsAdapter(Activity activity, Token token, OnAssetClickListener listener, boolean isGrid) + public NFTAssetsAdapter(Activity activity, Token token, OnAssetClickListener listener, OpenSeaService openSeaSvs, boolean isGrid) { this.activity = activity; this.listener = listener; this.token = token; this.isGrid = isGrid; + this.openSeaService = openSeaSvs; actualData = new ArrayList<>(); switch (token.getInterfaceSpec()) @@ -56,6 +60,7 @@ public NFTAssetsAdapter(Activity activity, Token token, OnAssetClickListener lis case ERC721_LEGACY: case ERC721_TICKET: case ERC721_UNDETERMINED: + case ERC721_ENUMERABLE: for (BigInteger i : token.getUniqueTokenIds()) { NFTAsset asset = token.getAssetForToken(i); @@ -136,7 +141,7 @@ private void displayAsset(@NotNull ViewHolder holder, NFTAsset asset, BigInteger holder.subtitle.setVisibility(View.GONE); } - if (!asset.needsLoading()) + if (asset.hasImageAsset()) { holder.icon.setupTokenImageThumbnail(asset); } @@ -151,18 +156,47 @@ private void displayAsset(@NotNull ViewHolder holder, NFTAsset asset, BigInteger } private void fetchAsset(ViewHolder holder, Pair pair) + { + if (EthereumNetworkBase.hasOpenseaAPI(token.tokenInfo.chainId)) + { + pair.second.metaDataLoader = openSeaService.getAsset(token, pair.first) + .map(NFTAsset::new) + .map(asset -> storeAsset(pair.first, asset, pair.second)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(asset -> checkAsset(asset, holder, pair), e -> {}); + } + else + { + fetchContractMetadata(holder, pair); + } + } + + private void fetchContractMetadata(ViewHolder holder, Pair pair) { pair.second.metaDataLoader = Single.fromCallable(() -> { - return token.fetchTokenMetadata(pair.first); //fetch directly from token - }).map(newAsset -> storeAsset(pair.first, newAsset, pair.second)) + return token.fetchTokenMetadata(pair.first); //fetch directly from token + }).map(newAsset -> storeAsset(pair.first, newAsset, pair.second)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(a -> displayAsset(holder, a, pair.first), e -> { - }); + .subscribe(a -> displayAsset(holder, a, pair.first), e -> {}); + } + + private void checkAsset(NFTAsset asset, ViewHolder holder, Pair pair) + { + if (asset.hasImageAsset()) + { + displayAsset(holder, asset, pair.first); + } + else + { + fetchContractMetadata(holder, pair); + } } private NFTAsset storeAsset(BigInteger tokenId, NFTAsset fetchedAsset, NFTAsset oldAsset) { + if (!fetchedAsset.hasImageAsset()) return oldAsset; fetchedAsset.updateFromRaw(oldAsset); if (activity != null && activity instanceof NFTActivity) { diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java index 129324d9c1..7b1ac9e0bc 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java @@ -1,6 +1,7 @@ package com.alphawallet.app.ui.widget.adapter; -import android.app.Activity; +import static com.alphawallet.app.service.AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME; + import android.content.Context; import android.util.Pair; import android.view.ViewGroup; @@ -29,11 +30,12 @@ import com.alphawallet.app.ui.widget.holder.BinderViewHolder; import com.alphawallet.app.ui.widget.holder.NFTAssetHolder; import com.alphawallet.app.ui.widget.holder.QuantitySelectorHolder; +import com.alphawallet.app.ui.widget.holder.TextHolder; import com.alphawallet.app.ui.widget.holder.TicketHolder; import com.alphawallet.app.ui.widget.holder.TokenDescriptionHolder; import com.alphawallet.app.ui.widget.holder.TotalBalanceHolder; -import com.alphawallet.app.web3.entity.FunctionCallback; import com.alphawallet.token.entity.TicketRange; +import com.alphawallet.token.entity.ViewType; import com.bumptech.glide.Glide; import org.jetbrains.annotations.NotNull; @@ -47,8 +49,6 @@ import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import static com.alphawallet.app.service.AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME; - /** * Created by James on 9/02/2018. */ @@ -58,31 +58,29 @@ public class NonFungibleTokenAdapter extends TokensAdapter implements NonFungibl TicketRange currentRange = null; final Token token; protected final OpenSeaService openseaService; - private final boolean clickThrough; + private final ViewType clickThrough; protected int assetCount; - private FunctionCallback functionCallback; - private final Activity activity; private boolean isGrid; public NonFungibleTokenAdapter(TokensAdapterCallback tokenClickListener, Token t, AssetDefinitionService service, - OpenSeaService opensea, Activity activity) { + OpenSeaService opensea) + { super(tokenClickListener, service); assetCount = 0; token = t; - clickThrough = true; + clickThrough = ViewType.ITEM_VIEW; openseaService = opensea; setToken(t); - this.activity = activity; } public NonFungibleTokenAdapter(TokensAdapterCallback tokenClickListener, Token t, AssetDefinitionService service, - OpenSeaService opensea, Activity activity, boolean isGrid) { + OpenSeaService opensea, boolean isGrid) + { super(tokenClickListener, service); assetCount = 0; token = t; - clickThrough = true; + clickThrough = ViewType.ITEM_VIEW; openseaService = opensea; - this.activity = activity; this.isGrid = isGrid; setToken(t); } @@ -93,10 +91,9 @@ public NonFungibleTokenAdapter(TokensAdapterCallback tokenClickListener, Token t super(tokenClickListener, service); assetCount = 0; token = t; - clickThrough = false; + clickThrough = ViewType.VIEW; openseaService = null; setTokenRange(token, tokenSelection); - this.activity = null; } public NonFungibleTokenAdapter(TokensAdapterCallback tokenClickListener, Token t, ArrayList> assetSelection, @@ -105,10 +102,9 @@ public NonFungibleTokenAdapter(TokensAdapterCallback tokenClickListener, Token t super(tokenClickListener, service); assetCount = 0; token = t; - clickThrough = false; + clickThrough = ViewType.VIEW; openseaService = null; setAssetSelection(token, assetSelection); - this.activity = null; } private void setAssetSelection(Token token, List> selection) @@ -118,9 +114,11 @@ private void setAssetSelection(Token token, List> sel @NotNull @Override - public BinderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + public BinderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { BinderViewHolder holder = null; - switch (viewType) { + switch (viewType) + { case TicketHolder.VIEW_TYPE: //Ticket holder now deprecated //TODO: remove holder = new TicketHolder(R.layout.item_ticket, parent, token, assetService); holder.setOnTokenClickListener(tokensAdapterCallback); @@ -141,14 +139,19 @@ public BinderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int vie case QuantitySelectorHolder.VIEW_TYPE: holder = new QuantitySelectorHolder(R.layout.item_quantity_selector, parent, assetCount, assetService); break; + default: + holder = new TextHolder(R.layout.item_standard_header, parent); + break; } return holder; } - public int getTicketRangeCount() { + public int getTicketRangeCount() + { int count = 0; - if (currentRange != null) { + if (currentRange != null) + { count = currentRange.tokenIds.size(); } return count; @@ -197,7 +200,7 @@ private void setAssetRange(Token t, List> selection) for (int i = 0; i < selection.size(); i++) { - items.add(new NFTSortedItem(selection.get(i), i+1)); + items.add(new NFTSortedItem(selection.get(i), i + 1)); } items.endBatchedUpdates(); @@ -256,7 +259,7 @@ protected SortedList addSortedItems(List sortedList, { currentRange = new TicketRange(e.id, t.getAddress()); final T item = generateType(currentRange, 10 + i, id); - items.add((SortedItem)item); + items.add((SortedItem) item); currentTime = e.time; } } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/RouteAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/RouteAdapter.java new file mode 100644 index 0000000000..65fc34101e --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/RouteAdapter.java @@ -0,0 +1,112 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.lifi.Route; +import com.alphawallet.app.ui.widget.entity.OnRouteSelectedListener; +import com.alphawallet.app.util.SwapUtils; +import com.alphawallet.app.widget.AddressIcon; +import com.google.android.material.card.MaterialCardView; + +import java.util.List; + +public class RouteAdapter extends RecyclerView.Adapter +{ + private final Context context; + private final List data; + private final OnRouteSelectedListener listener; + + public RouteAdapter(Context context, List data, OnRouteSelectedListener listener) + { + this.context = context; + this.data = data; + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { + int buttonTypeId = R.layout.item_route; + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(buttonTypeId, parent, false); + return new ViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) + { + Route item = data.get(position); + if (item != null) + { + Route.Step step = item.steps.get(0); + + holder.provider.setText(context.getString(R.string.label_swap_via, step.swapProvider.name)); + + for (String tag : item.tags) + { + if (tag.equalsIgnoreCase("RECOMMENDED")) + { + holder.tag.setVisibility(View.VISIBLE); + holder.tag.setText(tag); + } + } + + holder.value.setText(SwapUtils.getFormattedMinAmount(step.estimate, step.action)); + holder.icon.bindData(step.action.toToken.logoURI, step.action.toToken.chainId, step.action.toToken.address, step.action.toToken.symbol); +// holder.symbol.setText(step.action.toToken.symbol); + holder.gas.setText(context.getString(R.string.info_gas_fee, SwapUtils.getTotalGasFees(step.estimate.gasCosts))); + if (step.estimate.feeCosts != null && step.estimate.feeCosts.isEmpty()) + { + holder.fees.setVisibility(View.VISIBLE); + holder.fees.setText(SwapUtils.getOtherFees(step.estimate.feeCosts)); + } + else + { + holder.fees.setVisibility(View.GONE); + } + holder.layout.setOnClickListener(v -> listener.onRouteSelected(step.swapProvider.key)); + } + } + + @Override + public int getItemCount() + { + return data.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder + { + MaterialCardView layout; + TextView tag; + TextView provider; + TextView value; + TextView symbol; + TextView gas; + TextView fees; + TextView price; + AddressIcon icon; + + ViewHolder(View view) + { + super(view); + layout = view.findViewById(R.id.layout); + tag = view.findViewById(R.id.tag); + provider = view.findViewById(R.id.provider); + value = view.findViewById(R.id.value); + symbol = view.findViewById(R.id.symbol); + gas = view.findViewById(R.id.gas); + fees = view.findViewById(R.id.fees); + price = view.findViewById(R.id.price); + icon = view.findViewById(R.id.token_icon); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectChainAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectChainAdapter.java index b7e210f0fa..3f8f502b74 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectChainAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectChainAdapter.java @@ -1,11 +1,9 @@ package com.alphawallet.app.ui.widget.adapter; - import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; @@ -14,7 +12,7 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.lifi.Chain; import com.alphawallet.app.widget.SwapSettingsDialog; -import com.bumptech.glide.Glide; +import com.alphawallet.app.widget.TokenIcon; import com.google.android.material.radiobutton.MaterialRadioButton; import java.util.List; @@ -51,11 +49,7 @@ public void onBindViewHolder(ViewHolder holder, int position) { holder.name.setText(item.metamask.chainName); holder.chainId.setText(context.getString(R.string.chain_id, item.id)); - - Glide.with(context) - .load(item.logoURI) - .circleCrop() - .into(holder.chainIcon); + holder.chainIcon.bindData(item.id); if (item.id == selectedChainId) { @@ -75,18 +69,18 @@ public int getItemCount() public void setChains(List chains) { this.chains = chains; - notifyDataSetChanged(); + notifyItemRangeChanged(0, getItemCount()); } - public void setSelectedChain(long selectedChainId) + public long getSelectedChain() { - this.selectedChainId = selectedChainId; - notifyDataSetChanged(); + return this.selectedChainId; } - public long getSelectedChain() + public void setSelectedChain(long selectedChainId) { - return this.selectedChainId; + this.selectedChainId = selectedChainId; + notifyItemRangeChanged(0, getItemCount()); } static class ViewHolder extends RecyclerView.ViewHolder @@ -95,7 +89,7 @@ static class ViewHolder extends RecyclerView.ViewHolder TextView name; TextView chainId; View itemLayout; - ImageView chainIcon; + TokenIcon chainIcon; ViewHolder(View view) { diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectTokenAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectTokenAdapter.java index 01efa6769e..c5f32d6305 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectTokenAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SelectTokenAdapter.java @@ -1,6 +1,5 @@ package com.alphawallet.app.ui.widget.adapter; - import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -11,25 +10,24 @@ import androidx.recyclerview.widget.RecyclerView; import com.alphawallet.app.R; -import com.alphawallet.app.entity.lifi.Connection; +import com.alphawallet.app.entity.lifi.Token; import com.alphawallet.app.widget.AddressIcon; import com.alphawallet.app.widget.SelectTokenDialog; import com.google.android.material.radiobutton.MaterialRadioButton; import java.util.ArrayList; import java.util.List; -import java.util.Locale; public class SelectTokenAdapter extends RecyclerView.Adapter { - private final List tokens; - private final List displayData; + private final List displayData; private final SelectTokenDialog.SelectTokenDialogEventListener callback; + private final TokenFilter tokenFilter; private String selectedTokenAddress; - public SelectTokenAdapter(List tokens, SelectTokenDialog.SelectTokenDialogEventListener callback) + public SelectTokenAdapter(List tokens, SelectTokenDialog.SelectTokenDialogEventListener callback) { - this.tokens = tokens; + tokenFilter = new TokenFilter(tokens); this.callback = callback; displayData = new ArrayList<>(); displayData.addAll(tokens); @@ -48,7 +46,7 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - Connection.LToken item = displayData.get(position); + Token item = displayData.get(position); if (item != null) { holder.name.setText(item.name); @@ -57,7 +55,7 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) holder.name.append(")"); holder.tokenIcon.bindData(item.logoURI, item.chainId, selectedTokenAddress, item.symbol); - + String balance = item.balance; if (!TextUtils.isEmpty(balance)) { @@ -76,24 +74,12 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) } } - public void filter(String searchFilter) + public void filter(String keyword) { - List filteredList = new ArrayList<>(); - for (Connection.LToken data : tokens) - { - if (data.name.toLowerCase(Locale.ENGLISH).contains(searchFilter.toLowerCase(Locale.ENGLISH))) - { - filteredList.add(data); - } - else if (data.symbol.toLowerCase(Locale.ENGLISH).contains(searchFilter.toLowerCase(Locale.ENGLISH))) - { - filteredList.add(data); - } - } - updateList(filteredList); + updateList(tokenFilter.filterBy(keyword)); } - public void updateList(List filteredList) + public void updateList(List filteredList) { displayData.clear(); displayData.addAll(filteredList); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SingleSelectNetworkAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SingleSelectNetworkAdapter.java index 26da36152e..d072f1c255 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SingleSelectNetworkAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SingleSelectNetworkAdapter.java @@ -96,7 +96,7 @@ public int getItemCount() public void selectDefault() { - if (!hasSelection) + if (!hasSelection && !networkList.isEmpty()) { networkList.get(0).setSelected(true); notifyItemChanged(0); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SwapProviderAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SwapProviderAdapter.java new file mode 100644 index 0000000000..dffec42370 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/SwapProviderAdapter.java @@ -0,0 +1,96 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.lifi.SwapProvider; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.DrawableImageViewTarget; +import com.google.android.material.checkbox.MaterialCheckBox; + +import java.util.List; + +public class SwapProviderAdapter extends RecyclerView.Adapter +{ + private final List data; + private final Context context; + + public SwapProviderAdapter(Context context, List data) + { + this.context = context; + this.data = data; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { + int buttonTypeId = R.layout.item_exchange; + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(buttonTypeId, parent, false); + return new ViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) + { + SwapProvider item = data.get(position); + if (item != null) + { + holder.title.setText(item.name); + + holder.subtitle.setText(item.url); + + Glide.with(context) + .load(item.logoURI) + .placeholder(R.drawable.ic_logo) + .circleCrop() + .into(new DrawableImageViewTarget(holder.icon)); + + holder.layout.setOnClickListener(v -> holder.checkBox.setChecked(!item.isChecked)); + + holder.checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> item.isChecked = isChecked); + + holder.checkBox.setChecked(item.isChecked); + } + } + + public List getExchanges() + { + return data; + } + + @Override + public int getItemCount() + { + return data.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder + { + RelativeLayout layout; + AppCompatImageView icon; + TextView title; + TextView subtitle; + MaterialCheckBox checkBox; + + ViewHolder(View view) + { + super(view); + layout = view.findViewById(R.id.layout_list_item); + icon = view.findViewById(R.id.token_icon); + title = view.findViewById(R.id.provider); + subtitle = view.findViewById(R.id.subtitle); + checkBox = view.findViewById(R.id.checkbox); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TSAttributesAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TSAttributesAdapter.java new file mode 100644 index 0000000000..3a3a51e463 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TSAttributesAdapter.java @@ -0,0 +1,72 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.R; +import com.alphawallet.token.entity.TokenScriptResult; + +import java.util.ArrayList; +import java.util.List; + +public class TSAttributesAdapter extends RecyclerView.Adapter +{ + private final List attrList; + + public TSAttributesAdapter(List attrs) + { + this.attrList = new ArrayList<>(); + for (TokenScriptResult.Attribute attr : attrs) + { + if (!TextUtils.isEmpty(attr.name)) + { + this.attrList.add(attr); + } + } + } + + @NonNull + @Override + public TSAttributesAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) + { + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_attribute, viewGroup, false); + return new TSAttributesAdapter.ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull TSAttributesAdapter.ViewHolder viewHolder, int i) + { + TokenScriptResult.Attribute attr = attrList.get(i); + viewHolder.trait.setText(attr.name); + viewHolder.value.setText(attr.attrValue()); + viewHolder.rarity.setVisibility(View.GONE); + } + + @Override + public int getItemCount() + { + return attrList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder + { + TextView trait; + TextView value; + TextView rarity; + + ViewHolder(@NonNull View itemView) + { + super(itemView); + trait = itemView.findViewById(R.id.trait); + value = itemView.findViewById(R.id.value); + rarity = itemView.findViewById(R.id.rarity); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TestNetHorizontalListAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TestNetHorizontalListAdapter.java new file mode 100644 index 0000000000..6e5fada14a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TestNetHorizontalListAdapter.java @@ -0,0 +1,79 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.content.Context; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.alphawallet.app.R; +import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.widget.TokenIcon; + +import timber.log.Timber; + +public class TestNetHorizontalListAdapter extends RecyclerView.Adapter +{ + private final Token[] tokens; + private final Context context; + + public TestNetHorizontalListAdapter(Token[] tokens, Context context) + { + this.tokens = tokens; + this.context = context; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_horizontal_testnet_list, parent, false); + return new TestNetHorizontalListAdapter.ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) + { + holder.tokenIcon.clearLoad(); + try + { + String coinBalance = tokens[position].getStringBalanceForUI(4); + if (!TextUtils.isEmpty(coinBalance)) + { + holder.tokenPrice.setText(context.getString(R.string.valueSymbol, coinBalance, tokens[position].getTokenSymbol(tokens[position]))); + } + holder.tokenIcon.bindData(tokens[position].tokenInfo.chainId); + if (!tokens[position].isEthereum()) + { + holder.tokenIcon.setChainIcon(tokens[position].tokenInfo.chainId); //Add in when we upgrade the design + } + } + catch (Exception e) + { + Timber.e(e); + } + } + + @Override + public int getItemCount() + { + return tokens.length; + } + + static class ViewHolder extends RecyclerView.ViewHolder + { + TokenIcon tokenIcon; + TextView tokenPrice; + ViewHolder(@NonNull View itemView) + { + super(itemView); + tokenIcon = itemView.findViewById(R.id.token_icon); + tokenPrice = itemView.findViewById(R.id.title_set_price); + } + } +} + diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenFilter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenFilter.java new file mode 100644 index 0000000000..1e9fb39653 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenFilter.java @@ -0,0 +1,77 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.alphawallet.app.entity.lifi.Token; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; + +public class TokenFilter +{ + private final List tokens; + + public TokenFilter(List tokens) + { + this.tokens = tokens; + removeBadTokens(); + } + + private void removeBadTokens() + { + ListIterator iterator = this.tokens.listIterator(); + while (iterator.hasNext()) + { + Token t = iterator.next(); + if (TextUtils.isEmpty(t.name) || TextUtils.isEmpty(t.symbol)) + { + iterator.remove(); + } + } + } + + public List filterBy(String keyword) + { + String lowerCaseKeyword = lowerCase(keyword); + + List result = new ArrayList<>(); + // First filter: Add all entries that start with the keyword on top of the list. + for (Token lToken : this.tokens) + { + String name = lowerCase(lToken.name); + String symbol = lowerCase(lToken.symbol); + + if (name.startsWith(lowerCaseKeyword) || symbol.startsWith(lowerCaseKeyword)) + { + result.add(lToken); + } + } + + // Second filter: Add the rest of the entries that contain the keyword on top of the list. + for (Token lToken : this.tokens) + { + String name = lowerCase(lToken.name); + String symbol = lowerCase(lToken.symbol); + + if (name.contains(lowerCaseKeyword) || symbol.contains(lowerCaseKeyword)) + { + if (!result.contains(lToken)) + { + result.add(lToken); + } + } + } + return result; + } + + @NonNull + private String lowerCase(String name) + { + return name.toLowerCase(Locale.ENGLISH); + } + +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenListAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenListAdapter.java index 09806375c3..0595b51551 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenListAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenListAdapter.java @@ -1,5 +1,14 @@ package com.alphawallet.app.ui.widget.adapter; +import static com.alphawallet.app.entity.TokenManageType.DISPLAY_TOKEN; +import static com.alphawallet.app.entity.TokenManageType.HIDDEN_TOKEN; +import static com.alphawallet.app.entity.TokenManageType.LABEL_DISPLAY_TOKEN; +import static com.alphawallet.app.entity.TokenManageType.LABEL_HIDDEN_TOKEN; +import static com.alphawallet.app.entity.TokenManageType.LABEL_POPULAR_TOKEN; +import static com.alphawallet.app.entity.TokenManageType.POPULAR_TOKEN; +import static com.alphawallet.app.entity.TokenManageType.SHOW_ZERO_BALANCE; +import static com.alphawallet.app.repository.SharedPreferenceRepository.HIDE_ZERO_BALANCE_TOKENS; + import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; @@ -40,15 +49,6 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import static com.alphawallet.app.entity.TokenManageType.DISPLAY_TOKEN; -import static com.alphawallet.app.entity.TokenManageType.HIDDEN_TOKEN; -import static com.alphawallet.app.entity.TokenManageType.LABEL_DISPLAY_TOKEN; -import static com.alphawallet.app.entity.TokenManageType.LABEL_HIDDEN_TOKEN; -import static com.alphawallet.app.entity.TokenManageType.LABEL_POPULAR_TOKEN; -import static com.alphawallet.app.entity.TokenManageType.POPULAR_TOKEN; -import static com.alphawallet.app.entity.TokenManageType.SHOW_ZERO_BALANCE; -import static com.alphawallet.app.repository.SharedPreferenceRepository.HIDE_ZERO_BALANCE_TOKENS; - public class TokenListAdapter extends RecyclerView.Adapter implements OnTokenManageClickListener { private final Context context; @@ -152,6 +152,10 @@ private void setupList(List tokens, boolean forFilter) TokenSortedItem sortedItem = null; if (tokenCardMeta.isEthereum()) continue; //no chain cards Token token = tokensService.getToken(tokenCardMeta.getChain(), tokenCardMeta.getAddress()); + if (token == null) + { + continue; + } tokenCardMeta.isEnabled = token.tokenInfo.isEnabled; if (token.tokenInfo.isEnabled) @@ -466,4 +470,4 @@ else if (!isContractPopularToken(token.getAddress())) items.endBatchedUpdates(); notifyDataSetChanged(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java index a852f4d72f..0fe34d2d08 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java @@ -14,6 +14,7 @@ import com.alphawallet.app.entity.TokenFilter; import com.alphawallet.app.entity.tokendata.TokenGroup; import com.alphawallet.app.entity.tokens.TokenCardMeta; +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; import com.alphawallet.app.repository.TokensMappingRepository; import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.service.AssetDefinitionService; @@ -28,6 +29,7 @@ import com.alphawallet.app.ui.widget.entity.TestNetTipsItem; import com.alphawallet.app.ui.widget.entity.TokenSortedItem; import com.alphawallet.app.ui.widget.entity.TotalBalanceSortedItem; +import com.alphawallet.app.ui.widget.entity.WalletConnectSessionSortedItem; import com.alphawallet.app.ui.widget.entity.WarningData; import com.alphawallet.app.ui.widget.entity.WarningSortedItem; import com.alphawallet.app.ui.widget.holder.AssetInstanceScriptHolder; @@ -40,13 +42,16 @@ import com.alphawallet.app.ui.widget.holder.TokenGridHolder; import com.alphawallet.app.ui.widget.holder.TokenHolder; import com.alphawallet.app.ui.widget.holder.TotalBalanceHolder; +import com.alphawallet.app.ui.widget.holder.WalletConnectSessionHolder; import com.alphawallet.app.ui.widget.holder.WarningHolder; +import com.alphawallet.token.entity.ViewType; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -public class TokensAdapter extends RecyclerView.Adapter { +public class TokensAdapter extends RecyclerView.Adapter +{ private static final String TAG = "TKNADAPTER"; private TokenFilter filterType = TokenFilter.ALL; @@ -59,41 +64,50 @@ public class TokensAdapter extends RecyclerView.Adapter { private boolean debugView = false; private boolean gridFlag; + private boolean showTestNetTips = false; protected final TokensAdapterCallback tokensAdapterCallback; - protected final SortedList items = new SortedList<>(SortedItem.class, new SortedList.Callback() { + protected final SortedList items = new SortedList<>(SortedItem.class, new SortedList.Callback() + { @Override - public int compare(SortedItem o1, SortedItem o2) { + public int compare(SortedItem o1, SortedItem o2) + { return o1.compare(o2); } @Override - public void onChanged(int position, int count) { + public void onChanged(int position, int count) + { notifyItemRangeChanged(position, count); } @Override - public boolean areContentsTheSame(SortedItem oldItem, SortedItem newItem) { + public boolean areContentsTheSame(SortedItem oldItem, SortedItem newItem) + { return oldItem.areContentsTheSame(newItem); } @Override - public boolean areItemsTheSame(SortedItem item1, SortedItem item2) { + public boolean areItemsTheSame(SortedItem item1, SortedItem item2) + { return item1.areItemsTheSame(item2); } @Override - public void onInserted(int position, int count) { + public void onInserted(int position, int count) + { notifyItemRangeInserted(position, count); } @Override - public void onRemoved(int position, int count) { + public void onRemoved(int position, int count) + { notifyItemRangeRemoved(position, count); } @Override - public void onMoved(int fromPosition, int toPosition) { + public void onMoved(int fromPosition, int toPosition) + { notifyItemMoved(fromPosition, toPosition); } }); @@ -112,7 +126,8 @@ public TokensAdapter(TokensAdapterCallback tokensAdapterCallback, AssetDefinitio new TokensMappingRepository(aService.getTokenLocalSource()); } - protected TokensAdapter(TokensAdapterCallback tokensAdapterCallback, AssetDefinitionService aService) { + protected TokensAdapter(TokensAdapterCallback tokensAdapterCallback, AssetDefinitionService aService) + { this.tokensAdapterCallback = tokensAdapterCallback; this.assetService = aService; this.tokensService = null; @@ -122,16 +137,20 @@ protected TokensAdapter(TokensAdapterCallback tokensAdapterCallback, AssetDefini } @Override - public long getItemId(int position) { + public long getItemId(int position) + { Object obj = items.get(position); - if (obj instanceof TokenSortedItem) { + if (obj instanceof TokenSortedItem) + { TokenCardMeta tcm = ((TokenSortedItem) obj).value; - // This is an attempt to obtain a 'unique' id - // to fully utilise the RecyclerView's setHasStableIds feature. - // This will drastically reduce 'blinking' when the list changes + // This is an attempt to obtain a 'unique' id + // to fully utilise the RecyclerView's setHasStableIds feature. + // This will drastically reduce 'blinking' when the list changes return tcm.getUID(); - } else { + } + else + { return position; } } @@ -178,15 +197,19 @@ public BinderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int vie holder = new WarningHolder(R.layout.item_warning, parent); break; + case WalletConnectSessionHolder.VIEW_TYPE: + holder = new WalletConnectSessionHolder(R.layout.item_wallet_connect_sessions, parent); + break; + case AssetInstanceScriptHolder.VIEW_TYPE: - holder = new AssetInstanceScriptHolder(R.layout.item_ticket, parent, null, assetService, false); + holder = new AssetInstanceScriptHolder(R.layout.item_ticket, parent, null, assetService, ViewType.VIEW); break; case ChainNameHeaderHolder.VIEW_TYPE: holder = new ChainNameHeaderHolder(R.layout.item_chainname_header, parent); break; - // NB to save ppl a lot of effort this view doesn't show - item_total_balance has height coded to 1dp. + // NB to save ppl a lot of effort this view doesn't show - item_total_balance has height coded to 1dp. case TotalBalanceHolder.VIEW_TYPE: default: holder = new TotalBalanceHolder(R.layout.item_total_balance, parent); @@ -196,14 +219,15 @@ public BinderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int vie } @Override - public void onBindViewHolder(BinderViewHolder holder, int position) { + public void onBindViewHolder(BinderViewHolder holder, int position) + { items.get(position).view = holder; holder.bind(items.get(position).value); } public void onRViewRecycled(RecyclerView.ViewHolder holder) { - ((BinderViewHolder)holder).onDestroyView(); + ((BinderViewHolder) holder).onDestroyView(); } @Override @@ -213,7 +237,8 @@ public void onViewDetachedFromWindow(@NonNull BinderViewHolder holder) } @Override - public int getItemViewType(int position) { + public int getItemViewType(int position) + { if (position < items.size()) { return items.get(position).viewType; @@ -225,16 +250,20 @@ public int getItemViewType(int position) { } @Override - public int getItemCount() { + public int getItemCount() + { return items.size(); } - public void setWalletAddress(String walletAddress) { + public void setWalletAddress(String walletAddress) + { this.walletAddress = walletAddress; } - private void addSearchTokensLayout() { - if (walletAddress != null && !walletAddress.isEmpty()) { + private void addSearchTokensLayout() + { + if (walletAddress != null && !walletAddress.isEmpty()) + { items.add(new ManageTokensSearchItem(new ManageTokensData(walletAddress, managementLauncher), -1)); } } @@ -246,9 +275,11 @@ private void addHeaderLayout(TokenCardMeta tcm) items.add(new ChainItem(tcm.getChain(), tcm.group)); } - private void addManageTokensLayout() { + private void addManageTokensLayout() + { if (walletAddress != null && !walletAddress.isEmpty() && tokensService.isMainNetActive() - && (filterType == TokenFilter.ALL || filterType == TokenFilter.ASSETS)) { //only show buy button if filtering all or assets + && (filterType == TokenFilter.ALL || filterType == TokenFilter.ASSETS)) + { //only show buy button if filtering all or assets items.add(new ManageTokensSortedItem(new ManageTokensData(walletAddress, managementLauncher))); } } @@ -258,20 +289,12 @@ public void addWarning(WarningData data) items.add(new WarningSortedItem(data, 1)); } - public void removeBackupWarning() + public void setTokens(TokenCardMeta[] tokens) { - for (int i = 0; i < items.size(); i++) - { - if (items.get(i).viewType == WarningHolder.VIEW_TYPE) - { - items.removeItemAt(i); - notifyItemRemoved(i); - break; - } - } + populateTokens(tokens, true); } - public void setTokens(TokenCardMeta[] tokens) + public void updateTokenMetas(TokenCardMeta[] tokens) { populateTokens(tokens, false); } @@ -330,13 +353,17 @@ private void removeMatchingTokenDifferentWeight(TokenCardMeta token) } } - public void removeToken(TokenCardMeta token) { - for (int i = 0; i < items.size(); i++) { + public void removeToken(TokenCardMeta token) + { + for (int i = 0; i < items.size(); i++) + { Object si = items.get(i); - if (si instanceof TokenSortedItem) { + if (si instanceof TokenSortedItem) + { TokenSortedItem tsi = (TokenSortedItem) si; TokenCardMeta thisToken = tsi.value; - if (thisToken.tokenId.equalsIgnoreCase(token.tokenId)) { + if (thisToken.tokenId.equalsIgnoreCase(token.tokenId)) + { items.removeItemAt(i); break; } @@ -344,14 +371,18 @@ public void removeToken(TokenCardMeta token) { } } - public void removeToken(long chainId, String tokenAddress) { + public void removeToken(long chainId, String tokenAddress) + { String id = TokensRealmSource.databaseKey(chainId, tokenAddress); - for (int i = 0; i < items.size(); i++) { + for (int i = 0; i < items.size(); i++) + { Object si = items.get(i); - if (si instanceof TokenSortedItem) { + if (si instanceof TokenSortedItem) + { TokenSortedItem tsi = (TokenSortedItem) si; TokenCardMeta thisToken = tsi.value; - if (thisToken.tokenId.equalsIgnoreCase(id)) { + if (thisToken.tokenId.equalsIgnoreCase(id)) + { items.removeItemAt(i); break; } @@ -362,6 +393,11 @@ public void removeToken(long chainId, String tokenAddress) { private boolean canDisplayToken(TokenCardMeta token) { if (token == null) return false; + if (token.balance.equals("-2")) + { + return false; + } + //Add token to display list if it's the base currency, or if it has balance boolean allowThroughFilter = CustomViewSettings.tokenCanBeDisplayed(token); allowThroughFilter = checkTokenValue(token, allowThroughFilter); @@ -404,7 +440,8 @@ private boolean checkTokenValue(TokenCardMeta token, boolean allowThroughFilter) private void populateTokens(TokenCardMeta[] tokens, boolean clear) { items.beginBatchedUpdates(); - if (clear) { + if (clear) + { items.clear(); } @@ -425,13 +462,12 @@ private void populateTokens(TokenCardMeta[] tokens, boolean clear) private void addTestNetTips() { - if (!tokensService.isMainNetActive()) - { + if (!tokensService.isMainNetActive() && !showTestNetTips) items.add(new TestNetTipsItem(0)); - } } - public void setTotal(BigDecimal totalInCurrency) { + public void setTotal(BigDecimal totalInCurrency) + { total = new TotalBalanceSortedItem(totalInCurrency); //see if we need an update items.beginBatchedUpdates(); @@ -440,7 +476,7 @@ public void setTotal(BigDecimal totalInCurrency) { Object si = items.get(i); if (si instanceof TotalBalanceSortedItem) { - items.remove((TotalBalanceSortedItem)si); + items.remove((TotalBalanceSortedItem) si); items.add(total); notifyItemChanged(i); break; @@ -477,6 +513,12 @@ public void setFilterType(TokenFilter filterType) filterAdapterItems(); } + public void showTestNetTips() + { + this.showTestNetTips = true; + notifyDataSetChanged(); + } + public void clear() { items.beginBatchedUpdates(); @@ -505,8 +547,8 @@ public int getScrollPosition() Object si = items.get(i); if (si instanceof TokenSortedItem) { - TokenSortedItem tsi = (TokenSortedItem) si; - TokenCardMeta token = tsi.value; + TokenSortedItem tsi = (TokenSortedItem) si; + TokenCardMeta token = tsi.value; if (scrollToken.equals(token)) { scrollToken = null; @@ -561,4 +603,29 @@ public List getSelected() return selected; } + + public void showActiveWalletConnectSessions(List sessions) + { + if (sessions.isEmpty()) + { + removeItem(WalletConnectSessionHolder.VIEW_TYPE); + } + else + { + items.add(new WalletConnectSessionSortedItem(sessions, 2)); + } + } + + public void removeItem(int viewType) + { + for (int i = 0; i < items.size(); i++) + { + if (items.get(i).viewType == viewType) + { + items.removeItemAt(i); + notifyItemRemoved(i); + break; + } + } + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletAdapter.java new file mode 100644 index 0000000000..2ddf6d5692 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletAdapter.java @@ -0,0 +1,134 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.content.Context; +import android.graphics.Paint; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.widget.UserAvatar; +import com.google.android.material.checkbox.MaterialCheckBox; + +import java.util.ArrayList; +import java.util.List; + +public class WalletAdapter extends ArrayAdapter +{ + private Wallet defaultWallet; + private boolean[] selected; + private boolean readOnly; + + public WalletAdapter(Context context, Wallet[] wallets, Wallet defaultWallet) + { + super(context, 0, wallets); + selected = new boolean[wallets.length]; + this.defaultWallet = defaultWallet; + } + + public WalletAdapter(Context context, List wallets) + { + super(context, 0, wallets); + readOnly = true; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) + { + if (convertView == null) + { + convertView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_wallet, parent, false); + + convertView.setTag(new ViewHolder(convertView)); + } + + ViewHolder holder = (ViewHolder) convertView.getTag(); + + Wallet wallet = getItem(position); + + if (TextUtils.isEmpty(wallet.ENSname)) + { + holder.walletName.setVisibility(View.GONE); + holder.walletAddressSeparator.setVisibility(View.GONE); + } + else + { + holder.walletName.setText(wallet.ENSname); + } + holder.walletAddress.setText(Utils.formatAddress(wallet.address)); + if (wallet.type == WalletType.NOT_DEFINED) + { + holder.walletAddress.setPaintFlags(holder.walletAddress.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + holder.userAvatar.bind(wallet); + holder.balance.setText(String.format("%s %s", wallet.balance, wallet.balanceSymbol)); + + if (readOnly) + { + holder.checkbox.setVisibility(View.GONE); + } + else + { + if (wallet.address.equals(defaultWallet.address)) + { + holder.checkbox.setChecked(true); + selected[position] = true; + } + + holder.container.setOnClickListener(v -> + { + holder.checkbox.setChecked(!holder.checkbox.isChecked()); + selected[position] = !selected[position]; + }); + + } + return convertView; + } + + public List getSelectedWallets() + { + List selectedWallets = new ArrayList<>(); + for (int i = 0; i < getCount(); i++) + { + if (selected[i]) + { + selectedWallets.add(getItem(i)); + } + } + return selectedWallets; + } + + static class ViewHolder + { + View container; + TextView walletName; + TextView walletAddress; + TextView balance; + TextView walletAddressSeparator; + UserAvatar userAvatar; + MaterialCheckBox checkbox; + + public ViewHolder(@NonNull View view) + { + container = view; + walletName = view.findViewById(R.id.wallet_name); + walletAddress = view.findViewById(R.id.wallet_address); + balance = view.findViewById(R.id.wallet_balance); + walletAddressSeparator = view.findViewById(R.id.wallet_address_separator); + userAvatar = view.findViewById(R.id.wallet_icon); + checkbox = view.findViewById(R.id.checkbox); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletsSummaryAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletsSummaryAdapter.java index ebde1bdf52..433e7755fe 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletsSummaryAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/WalletsSummaryAdapter.java @@ -14,6 +14,7 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.WalletItem; import com.alphawallet.app.ui.widget.entity.WalletClickCallback; @@ -34,7 +35,7 @@ public class WalletsSummaryAdapter extends RecyclerView.Adapter implements WalletClickCallback, Runnable { private final OnSetWalletDefaultListener onSetWalletDefaultListener; - private boolean mainNetActivated; + private final boolean mainNetActivated; private final ArrayList wallets; private final Map> valueMap = new HashMap<>(); private final Handler handler = new Handler(Looper.getMainLooper()); @@ -45,7 +46,8 @@ public class WalletsSummaryAdapter extends RecyclerView.Adapter(); @@ -56,17 +58,19 @@ public WalletsSummaryAdapter(Context ctx, @NotNull @Override - public BinderViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) { + public BinderViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) + { BinderViewHolder binderViewHolder = null; - switch (viewType) { + switch (viewType) + { case WalletHolder.VIEW_TYPE: binderViewHolder = new WalletSummaryHolder(R.layout.item_wallet_summary_manage, parent, this, realm); - break; + break; case TextHolder.VIEW_TYPE: binderViewHolder = new TextHolder(R.layout.item_standard_header, parent); break; case WalletSummaryHeaderHolder.VIEW_TYPE: - binderViewHolder = new WalletSummaryHeaderHolder(R.layout.item_wallet_summary_large_title, parent,this, realm); + binderViewHolder = new WalletSummaryHeaderHolder(R.layout.item_wallet_summary_large_title, parent, this, realm); break; default: break; @@ -75,9 +79,11 @@ public BinderViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewTy } @Override - public void onBindViewHolder(@NotNull BinderViewHolder holder, int position) { + public void onBindViewHolder(@NotNull BinderViewHolder holder, int position) + { Bundle bundle; - switch (getItemViewType(position)) { + switch (getItemViewType(position)) + { case WalletHolder.VIEW_TYPE: Wallet wallet = wallets.get(position); bundle = new Bundle(); @@ -120,12 +126,14 @@ public void onBindViewHolder(@NotNull BinderViewHolder holder, int position) { } @Override - public int getItemCount() { + public int getItemCount() + { return wallets.size(); } @Override - public int getItemViewType(int position) { + public int getItemViewType(int position) + { switch (wallets.get(position).type) { default: @@ -141,7 +149,8 @@ public int getItemViewType(int position) { } } - public void setDefaultWallet(Wallet wallet) { + public void setDefaultWallet(Wallet wallet) + { this.defaultWallet = wallet; notifyDataSetChanged(); } @@ -307,7 +316,7 @@ public void onWalletClicked(Wallet wallet) public void ensAvatar(Wallet wallet) { //update the ENS avatar in the database - walletInteract.updateWalletItem(wallet, WalletItem.ENS_AVATAR, () -> { }); + walletInteract.updateWalletItem(wallet, WalletItem.ENS_AVATAR, () -> {}); } public void onDestroy() @@ -315,7 +324,34 @@ public void onDestroy() realm.close(); } - public interface OnSetWalletDefaultListener { + public int getDefaultWalletIndex() + { + if (defaultWallet != null) + { + return getWalletIndex(defaultWallet.address); + } + return -1; + } + + public interface OnSetWalletDefaultListener + { void onSetDefault(Wallet wallet); } + + public void setTokens(Map walletTokens) + { + if (walletTokens == null) return; + + for (Token[] token : walletTokens.values()) + { + Token[] t = walletTokens.get(token[0].getAddress()); + String walletAddress = token[0].getAddress(); + int walletIndex = getWalletIndex(walletAddress); + if (walletIndex != -1) + { + this.wallets.get(walletIndex).tokens = t; + notifyItemChanged(walletIndex); + } + } + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ActionSheetCallback.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ActionSheetCallback.java index 869bd2b3a1..00d76ab0e8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ActionSheetCallback.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ActionSheetCallback.java @@ -1,14 +1,14 @@ package com.alphawallet.app.ui.widget.entity; -import android.app.Activity; import android.content.Intent; import androidx.activity.result.ActivityResultLauncher; import com.alphawallet.app.entity.SignAuthenticationCallback; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.walletconnect.entity.WCPeerMeta; import com.alphawallet.app.web3.entity.Web3Transaction; +import com.alphawallet.token.entity.Signable; /** * Created by JB on 27/11/2020. @@ -16,16 +16,40 @@ public interface ActionSheetCallback { void getAuthorisation(SignAuthenticationCallback callback); + void sendTransaction(Web3Transaction tx); + void dismissed(String txHash, long callbackId, boolean actionCompleted); + void notifyConfirm(String mode); + ActivityResultLauncher gasSelectLauncher(); - default void signTransaction(Web3Transaction tx) { } // only WalletConnect uses this so far - default void buttonClick(long callbackId, Token baseToken) { }; //for message only actionsheet + default void signTransaction(Web3Transaction tx) + { + } // only WalletConnect uses this so far + + default void buttonClick(long callbackId, Token baseToken) + { + } + + default void notifyWalletConnectApproval(long chainId) + { + } + + default void denyWalletConnect() + { + } + + default void openChainSelection() + { + } + + default void signingComplete(SignatureFromKey signature, Signable message) + { + } - default void notifyWalletConnectApproval(long chainId) { }; // used by WalletConnectRequest - default void denyWalletConnect() { }; - default void openChainSelection() { }; // used by WalletConnectRequest - default void buttonClick(String action, int Id) { }; // for passing + default void signingFailed(Throwable error, Signable message) + { + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ChainItem.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ChainItem.java index e77a14606c..2e80b84d2f 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ChainItem.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ChainItem.java @@ -9,7 +9,7 @@ */ public class ChainItem extends SortedItem { - public static long CHAIN_ITEM_WEIGHT = 2; + public static long CHAIN_ITEM_WEIGHT = 4; public ChainItem(Long networkId, TokenGroup group) { super(ChainNameHeaderHolder.VIEW_TYPE, networkId, new TokenPosition(group, networkId, CHAIN_ITEM_WEIGHT)); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ENSHandler.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ENSHandler.java index 39b421441f..b5d99d6b0f 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ENSHandler.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ENSHandler.java @@ -4,23 +4,21 @@ * Created by JB on 28/10/2020. */ +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; + import android.content.Context; import android.os.Handler; import android.os.Looper; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.TypedValue; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import com.alphawallet.app.C; import com.alphawallet.app.R; -import com.alphawallet.app.entity.EnsNodeNotSyncCallback; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.ui.widget.adapter.AutoCompleteAddressAdapter; -import com.alphawallet.app.util.AWEnsResolver; +import com.alphawallet.app.util.ens.AWEnsResolver; import com.alphawallet.app.util.Utils; import com.alphawallet.app.widget.InputAddress; import com.google.gson.Gson; @@ -34,9 +32,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; /** * Created by James on 4/12/2018. @@ -66,7 +61,7 @@ public ENSHandler(InputAddress host, AutoCompleteAddressAdapter adapter) this.handler = new Handler(Looper.getMainLooper()); this.adapterUrl = adapter; this.host = host; - this.ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), host.getContext()); + this.ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), host.getContext(), host.getChain()); createWatcher(); getENSHistoryFromPrefs(host.getContext()); @@ -230,7 +225,7 @@ else if (canBeENSName(to)) host.setWaitingSpinner(true); host.ENSName(to); - disposable = ensResolver.resolveENSAddress(to, performEnsSync) + disposable = ensResolver.resolveENSAddress(to) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(resolvedAddress -> onENSSuccess(resolvedAddress, to), this::onENSError); @@ -335,9 +330,9 @@ private void storeHistory(HashMap history) PreferenceManager.getDefaultSharedPreferences(host.getContext()).edit().putString(C.ENS_HISTORY_PAIR, historyJson).apply(); } - public void setEnsNodeNotSyncCallback(EnsNodeNotSyncCallback callback) + /*public void setEnsNodeNotSyncCallback(EnsNodeNotSyncCallback callback) { Timber.d("setEnsNodeNotSyncCallback: "); ensResolver.nodeNotSyncCallback = callback; - } + }*/ } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java index 68da72215a..424a813b00 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java @@ -1,7 +1,7 @@ package com.alphawallet.app.ui.widget.entity; import com.alphawallet.app.web3.entity.Web3Transaction; -import com.alphawallet.app.widget.ActionSheetMode; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import java.math.BigDecimal; import java.math.BigInteger; diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/HeaderItem.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/HeaderItem.java index bdd409d525..44fa9155a0 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/HeaderItem.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/HeaderItem.java @@ -7,7 +7,7 @@ public class HeaderItem extends SortedItem { public HeaderItem(TokenGroup group) { - super(HeaderHolder.VIEW_TYPE, group, new TokenPosition(group, 1, 1, true)); + super(HeaderHolder.VIEW_TYPE, group, new TokenPosition(group, 1, 3, true)); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/HistoryChart.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/HistoryChart.java index 4604622b59..69b99723e7 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/HistoryChart.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/HistoryChart.java @@ -34,6 +34,7 @@ import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; import okhttp3.Request; +import timber.log.Timber; public class HistoryChart extends View { @@ -141,7 +142,7 @@ static Single fetchHistory(Range range, String tokenId) } catch (Exception e) { - e.printStackTrace(); + Timber.e(e); } return null; }); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/IconItem.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/IconItem.java index 61023be6c6..dbff547845 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/IconItem.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/IconItem.java @@ -2,8 +2,6 @@ import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; -import com.bumptech.glide.signature.ObjectKey; - import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -44,12 +42,6 @@ public static void secondaryFound(long chainId, String address) iconLoadType.put(databaseKey(chainId, address.toLowerCase()), true); } - //Use TextIcon - public static void noIconFound(long chainId, String address) - { - iconLoadType.put(databaseKey(chainId, address.toLowerCase()), false); - } - /** * Resets the failed icon fetch checking - try again to load failed icons */ diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/NFTAttributeLayout.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/NFTAttributeLayout.java index 752c3e03a5..258cc2eebc 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/NFTAttributeLayout.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/NFTAttributeLayout.java @@ -13,8 +13,10 @@ import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.opensea.OpenSeaAsset; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.ui.widget.adapter.TSAttributesAdapter; import com.alphawallet.app.ui.widget.adapter.TraitsAdapter; import com.alphawallet.app.widget.TokenInfoCategoryView; +import com.alphawallet.token.entity.TokenScriptResult; import java.util.ArrayList; import java.util.List; @@ -53,6 +55,13 @@ public void bind(Token token, List traits, long totalSupply) setAttributeLabel(token.tokenInfo.name, adapter.getItemCount()); } + public void bindTSAttributes(List attrs) + { + TSAttributesAdapter adapter = new TSAttributesAdapter(attrs); + recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + recyclerView.setAdapter(adapter); + } + private void setAttributeLabel(String tokenName, int size) { if (size > 0 && tokenName.equalsIgnoreCase("cryptokitties")) diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/OnRouteSelectedListener.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/OnRouteSelectedListener.java new file mode 100644 index 0000000000..2592a3bb57 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/OnRouteSelectedListener.java @@ -0,0 +1,6 @@ +package com.alphawallet.app.ui.widget.entity; + +public interface OnRouteSelectedListener +{ + void onRouteSelected(String provider); +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/ProgressInfo.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ProgressInfo.java new file mode 100644 index 0000000000..e5523168e5 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/ProgressInfo.java @@ -0,0 +1,28 @@ +package com.alphawallet.app.ui.widget.entity; + +public class ProgressInfo +{ + private boolean shouldShow; + private int messageRes; + + public ProgressInfo(boolean shouldShow, int messageRes) + { + this.shouldShow = shouldShow; + this.messageRes = messageRes; + } + + public ProgressInfo(boolean shouldShow) + { + this.shouldShow = shouldShow; + } + + public boolean shouldShow() + { + return shouldShow; + } + + public int getMessage() + { + return messageRes; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/TokenTransferData.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/TokenTransferData.java index ae537bd1ee..569e366b45 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/TokenTransferData.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/TokenTransferData.java @@ -168,7 +168,7 @@ public String getDetail(Context ctx, Transaction tx, final String itemView, Toke EventResult eResult = resultMap.get("from"); if (eResult != null) { - if (tx != null && eResult.value.equals(ZERO_ADDRESS)) + if (tx != null && eResult.value.equals(ZERO_ADDRESS) && t != null) { return t.getFullName(); } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/WalletConnectSessionSortedItem.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/WalletConnectSessionSortedItem.java new file mode 100644 index 0000000000..b28d84d0b0 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/WalletConnectSessionSortedItem.java @@ -0,0 +1,26 @@ +package com.alphawallet.app.ui.widget.entity; + +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.ui.widget.holder.WalletConnectSessionHolder; + +import java.util.List; + +public class WalletConnectSessionSortedItem extends SortedItem> +{ + public WalletConnectSessionSortedItem(List sessions, int weight) + { + super(WalletConnectSessionHolder.VIEW_TYPE, sessions, new TokenPosition(weight)); + } + + @Override + public boolean areContentsTheSame(SortedItem newItem) + { + return false; // always override the existed one + } + + @Override + public boolean areItemsTheSame(SortedItem other) + { + return other.viewType == viewType; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/AssetInstanceScriptHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/AssetInstanceScriptHolder.java index e924a3d0a7..1ef18f89ca 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/AssetInstanceScriptHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/AssetInstanceScriptHolder.java @@ -11,7 +11,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatRadioButton; import com.alphawallet.app.C; import com.alphawallet.app.R; @@ -23,6 +22,7 @@ import com.alphawallet.app.web3.Web3TokenView; import com.alphawallet.app.web3.entity.PageReadyCallback; import com.alphawallet.token.entity.TicketRange; +import com.alphawallet.token.entity.ViewType; import com.google.android.material.radiobutton.MaterialRadioButton; /** @@ -37,14 +37,14 @@ public class AssetInstanceScriptHolder extends BinderViewHolder imp private final Token token; private final LinearLayout clickWrapper; private final LinearLayout webWrapper; - private final boolean iconified; + private final ViewType iconified; private TokensAdapterCallback tokenClickListener; private final MaterialRadioButton itemSelect; private final AssetDefinitionService assetDefinitionService; //need to cache this locally, unless we cache every string we need in the constructor private boolean activeClick; private final Handler handler = new Handler(); - public AssetInstanceScriptHolder(int resId, ViewGroup parent, Token t, AssetDefinitionService assetService, boolean iconified) + public AssetInstanceScriptHolder(int resId, ViewGroup parent, Token t, AssetDefinitionService assetService, ViewType iconified) { super(resId, parent); tokenView = findViewById(R.id.web3_tokenview); @@ -79,7 +79,7 @@ public void bind(@Nullable TicketRange data, @NonNull Bundle addition) tokenView.displayTicketHolder(token, data, assetDefinitionService, iconified); tokenView.setOnReadyCallback(this); - if (iconified) + if (iconified == ViewType.ITEM_VIEW) { clickWrapper.setVisibility((View.VISIBLE)); clickWrapper.setOnClickListener(v -> handleClick(v, data)); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/BaseTicketHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/BaseTicketHolder.java index 68b37423c3..cc23338c89 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/BaseTicketHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/BaseTicketHolder.java @@ -1,23 +1,23 @@ package com.alphawallet.app.ui.widget.holder; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.LinearLayout; import android.widget.RelativeLayout; -import com.alphawallet.app.R; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.alphawallet.app.R; import com.alphawallet.app.entity.tokens.Token; - import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.web3.Web3TokenView; import com.alphawallet.app.web3.entity.PageReadyCallback; import com.alphawallet.token.entity.TicketRange; -import com.alphawallet.app.ui.widget.TokensAdapterCallback; +import com.alphawallet.token.entity.ViewType; public class BaseTicketHolder extends BinderViewHolder implements View.OnClickListener, View.OnLongClickListener, PageReadyCallback { @@ -51,7 +51,7 @@ public void bind(@Nullable TicketRange data, @NonNull Bundle addition) if (data.tokenIds.size() > 0) { - tokenView.displayTicketHolder(token, data, assetService, true); + tokenView.displayTicketHolder(token, data, assetService, ViewType.ITEM_VIEW); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TestNetTipsHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TestNetTipsHolder.java index 6c73899c47..18338a0231 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TestNetTipsHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TestNetTipsHolder.java @@ -1,6 +1,5 @@ package com.alphawallet.app.ui.widget.holder; -import android.app.Dialog; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransferHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransferHolder.java index 5abbc3fe5d..f858ce8c95 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransferHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransferHolder.java @@ -143,6 +143,11 @@ public void setFromTokenView() private String getEventAmount(TokenTransferData eventData, Transaction tx) { + if (token == null) + { + return ""; + } + tx.getDestination(token); //build decoded input Map resultMap = eventData.getEventResultMap(); String value = ""; diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletConnectSessionHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletConnectSessionHolder.java new file mode 100644 index 0000000000..01819e438f --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletConnectSessionHolder.java @@ -0,0 +1,37 @@ +package com.alphawallet.app.ui.widget.holder; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.ui.WalletConnectNotificationActivity; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class WalletConnectSessionHolder extends BinderViewHolder> +{ + public static final int VIEW_TYPE = 2024; + private final View container; + + public WalletConnectSessionHolder(int resId, ViewGroup parent) + { + super(resId, parent); + container = findViewById(R.id.layout_item_wallet_connect); + } + + public void bind(@Nullable List sessionItemList, @NonNull Bundle addition) + { + container.setOnClickListener(view -> onClick()); + } + + private void onClick() + { + getContext().startActivity(new Intent(getContext(), WalletConnectNotificationActivity.class)); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletSummaryHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletSummaryHolder.java index 1d7c275da5..15c79793bb 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletSummaryHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/WalletSummaryHolder.java @@ -12,6 +12,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; @@ -26,11 +27,13 @@ import com.alphawallet.app.ui.widget.entity.AvatarWriteCallback; import com.alphawallet.app.ui.widget.entity.WalletClickCallback; import com.alphawallet.app.util.Utils; +import com.alphawallet.app.widget.TokensBalanceView; import com.alphawallet.app.widget.UserAvatar; import java.math.BigDecimal; import java.math.RoundingMode; +import io.reactivex.disposables.Disposable; import io.realm.Realm; import io.realm.RealmResults; @@ -45,21 +48,23 @@ public class WalletSummaryHolder extends BinderViewHolder implements Vie private final ImageView defaultWalletIndicator; private final ImageView manageWalletBtn; private final UserAvatar walletIcon; - private final LinearLayout walletClickLayout; + private final RelativeLayout arrowRight; private final TextView walletBalanceText; private final TextView walletNameText; private final TextView walletAddressSeparator; private final TextView walletAddressText; private final TextView wallet24hChange; + private final TokensBalanceView tokensBalanceView; private final Realm realm; private RealmResults realmUpdate; - private final WalletClickCallback clickCallback; private Wallet wallet = null; + protected Disposable disposable; public WalletSummaryHolder(int resId, ViewGroup parent, WalletClickCallback callback, Realm realm) { super(resId, parent); + defaultWalletIndicator = findViewById(R.id.image_default_indicator); manageWalletBtn = findViewById(R.id.manage_wallet_btn); walletIcon = findViewById(R.id.wallet_icon); @@ -67,8 +72,9 @@ public WalletSummaryHolder(int resId, ViewGroup parent, WalletClickCallback call walletNameText = findViewById(R.id.wallet_name); walletAddressSeparator = findViewById(R.id.wallet_address_separator); walletAddressText = findViewById(R.id.wallet_address); - walletClickLayout = findViewById(R.id.wallet_click_layer); + arrowRight = findViewById(R.id.container); wallet24hChange = findViewById(R.id.wallet_24h_change); + tokensBalanceView = findViewById(R.id.token_with_balance_view); clickCallback = callback; manageWalletLayout = findViewById(R.id.layout_manage_wallet); this.realm = realm; @@ -82,8 +88,9 @@ public void bind(@Nullable Wallet data, @NonNull Bundle addition) if (data != null) { + tokensBalanceView.blankView(); wallet = fetchWallet(data); - walletClickLayout.setOnClickListener(this); + arrowRight.setOnClickListener(this); manageWalletLayout.setOnClickListener(this); if (addition.getBoolean(IS_DEFAULT_ADDITION, false)) @@ -141,7 +148,7 @@ else if (wallet.ENSname != null && wallet.ENSname.length() > 0) double oldFiatValue = addition.getDouble(FIAT_CHANGE, 0.00); String balanceTxt = TickerService.getCurrencyString(fiatValue); - + walletBalanceText.setVisibility(View.VISIBLE); walletBalanceText.setText(balanceTxt); setWalletChange(fiatValue != 0 ? ((fiatValue - oldFiatValue) / oldFiatValue) * 100.0 : 0.0); } @@ -183,7 +190,8 @@ private void startRealmListener() { realmUpdate = realm.where(RealmWalletData.class) .equalTo("address", wallet.address).findAllAsync(); - realmUpdate.addChangeListener(realmWallets -> { + realmUpdate.addChangeListener(realmWallets -> + { //update balance if (realmWallets.size() == 0) return; RealmWalletData realmWallet = realmWallets.first(); @@ -211,15 +219,18 @@ private Wallet fetchWallet(Wallet w) RealmWalletData realmWallet = realm.where(RealmWalletData.class) .equalTo("address", w.address) .findFirst(); - if (realmWallet != null) { w.balance = realmWallet.getBalance(); w.ENSname = realmWallet.getENSName(); w.name = realmWallet.getName(); w.ENSAvatar = realmWallet.getENSAvatar(); - } + if (w.tokens != null) + { + tokensBalanceView.bindTokens(w.tokens); + } + } return w; } @@ -264,7 +275,7 @@ public void onClick(View view) //if (wallet == null) { return; } //protect against click between constructor and bind switch (view.getId()) { - case R.id.wallet_click_layer: + case R.id.container: clickCallback.onWalletClicked(wallet); break; diff --git a/app/src/main/java/com/alphawallet/app/util/BalanceUtils.java b/app/src/main/java/com/alphawallet/app/util/BalanceUtils.java index aa000b8d1d..6c03800372 100644 --- a/app/src/main/java/com/alphawallet/app/util/BalanceUtils.java +++ b/app/src/main/java/com/alphawallet/app/util/BalanceUtils.java @@ -15,6 +15,9 @@ public class BalanceUtils { private static final String weiInEth = "1000000000000000000"; private static final int showDecimalPlaces = 5; + private static final int slidingDecimalPlaces = 2; + private static final BigDecimal displayThresholdMillis = BigDecimal.ONE.divide(BigDecimal.valueOf(1000000), 18, RoundingMode.DOWN); + private static final BigDecimal oneGwei = BigDecimal.ONE.divide(Convert.Unit.GWEI.getWeiFactor(), 18, RoundingMode.DOWN); //BigDecimal.valueOf(0.000000001); public static final String MACRO_PATTERN = "###,###,###,###,##0"; public static final String CURRENCY_PATTERN = MACRO_PATTERN + ".00"; @@ -192,6 +195,16 @@ else if (requiresSuffix(correctedValue, dPlaces)) return returnValue; } + public static boolean requiresSmallValueSuffix(BigDecimal correctedValue) + { + return correctedValue.compareTo(displayThresholdMillis) < 0; + } + + public static boolean requiresSmallGweiValueSuffix(BigDecimal ethAmount) + { + return ethAmount.compareTo(oneGwei) < 0; + } + private static boolean requiresSuffix(BigDecimal correctedValue, int dPlaces) { final BigDecimal displayThreshold = BigDecimal.ONE.divide(BigDecimal.valueOf(Math.pow(10, dPlaces)), 18, RoundingMode.DOWN); @@ -330,4 +343,60 @@ public static String getRawFormat(String amount, long decimals) BigDecimal a = new BigDecimal(amount); return a.movePointRight((int) decimals).toString(); } + + public static String getSlidingBaseValue(final BigDecimal value, int decimals, int dPlaces) + { + String returnValue; + BigDecimal correctedValue = value.divide(BigDecimal.valueOf(Math.pow(10, decimals)), 18, RoundingMode.DOWN); + + if (value.equals(BigDecimal.ZERO)) //zero balance + { + returnValue = "0"; + } + else if (requiresSmallValueSuffix(correctedValue)) + { + return smallSuffixValue(correctedValue); + } + else if (correctedValue.compareTo(displayThresholdMillis) < 0) + { + returnValue = correctedValue.divide(displayThresholdMillis, slidingDecimalPlaces, RoundingMode.DOWN) + " m"; + } + else if (requiresSuffix(correctedValue, dPlaces)) + { + returnValue = getSuffixedValue(correctedValue, dPlaces); + } + else //otherwise display in standard pattern to dPlaces dp + { + DecimalFormat df = getFormat(getDigitalPattern(dPlaces)); + //DecimalFormat df = new DecimalFormat(getDigitalPattern(dPlaces)); + df.setRoundingMode(RoundingMode.DOWN); + returnValue = convertToLocale(df.format(correctedValue)); + } + + return returnValue; + } + + private static String smallSuffixValue(BigDecimal correctedValue) + { + final BigDecimal displayThresholdMicro = BigDecimal.ONE.divide(BigDecimal.valueOf(1000000000), 18, RoundingMode.DOWN); + final BigDecimal displayThresholdNano = BigDecimal.ONE.divide(BigDecimal.valueOf(1000000000000L), 18, RoundingMode.DOWN); + + BigDecimal weiAmount = Convert.toWei(correctedValue, Convert.Unit.ETHER); + + DecimalFormat df = getFormat("###,###.##"); + df.setRoundingMode(RoundingMode.DOWN); + + if (correctedValue.compareTo(displayThresholdNano) < 0) + { + return weiAmount.longValue() + " wei"; + } + else if (correctedValue.compareTo(displayThresholdMicro) < 0) + { + return df.format(weiAmount.divide(BigDecimal.valueOf(1000), 2, RoundingMode.DOWN)) + "K wei"; + } + else + { + return df.format(weiAmount.divide(BigDecimal.valueOf(1000000), 2, RoundingMode.DOWN)) + "M wei"; + } + } } diff --git a/app/src/main/java/com/alphawallet/app/util/CoinbasePayUtils.java b/app/src/main/java/com/alphawallet/app/util/CoinbasePayUtils.java new file mode 100644 index 0000000000..a8aff83f98 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/CoinbasePayUtils.java @@ -0,0 +1,17 @@ +package com.alphawallet.app.util; + +import com.alphawallet.app.entity.coinbasepay.DestinationWallet; +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; + +public class CoinbasePayUtils +{ + public static String getDestWalletJson(DestinationWallet.Type type, String address, List value) + { + List destinationWallets = new ArrayList<>(); + destinationWallets.add(new DestinationWallet(type, address, value)); + return new Gson().toJson(destinationWallets); + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/DappBrowserUtils.java b/app/src/main/java/com/alphawallet/app/util/DappBrowserUtils.java index 06e4fc6a94..ee0901df3a 100644 --- a/app/src/main/java/com/alphawallet/app/util/DappBrowserUtils.java +++ b/app/src/main/java/com/alphawallet/app/util/DappBrowserUtils.java @@ -1,7 +1,8 @@ package com.alphawallet.app.util; -import static com.alphawallet.app.repository.EthereumNetworkBase.isWithinHomePage; import static com.alphawallet.app.util.Utils.isValidUrl; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_TEST_ID; import android.content.Context; import android.content.SharedPreferences; @@ -31,6 +32,8 @@ public class DappBrowserUtils { private static final String DAPPS_LIST_FILENAME = "dapps_list.json"; private static final String MY_DAPPS_FILE = "mydapps"; private static final String DAPPS_HISTORY_FILE = "dappshistory"; + private static final String DEFAULT_HOMEPAGE = "https://alphawallet.com/browser/"; + private static final String POLYGON_HOMEPAGE = "https://alphawallet.com/browser-item-category/polygon/"; //TODO: Move to database public static void saveToPrefs(Context context, List myDapps) { @@ -253,4 +256,21 @@ private static void blankPrefEntry(Context context, String key) .putString(key, "") .apply(); } + + public static String defaultDapp(long chainId) + { + return (chainId == POLYGON_ID || chainId == POLYGON_TEST_ID) ? POLYGON_HOMEPAGE : DEFAULT_HOMEPAGE; + } + + public static boolean isWithinHomePage(String url) + { + String homePageRoot = DEFAULT_HOMEPAGE.substring(0, DEFAULT_HOMEPAGE.length() - 1); //remove final slash + return (url != null && url.startsWith(homePageRoot)); + } + + public static boolean isDefaultDapp(String url) + { + return (DEFAULT_HOMEPAGE.equals(url) + || POLYGON_HOMEPAGE.equals(url)); + } } diff --git a/app/src/main/java/com/alphawallet/app/util/EnsResolver.java b/app/src/main/java/com/alphawallet/app/util/EnsResolver.java deleted file mode 100644 index c3ae165f69..0000000000 --- a/app/src/main/java/com/alphawallet/app/util/EnsResolver.java +++ /dev/null @@ -1,376 +0,0 @@ -package com.alphawallet.app.util; - -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; - -import com.alphawallet.app.BuildConfig; -import com.alphawallet.app.entity.EnsNodeNotSyncCallback; -import com.alphawallet.app.entity.UnableToResolveENS; -import com.alphawallet.app.entity.tokenscript.TokenscriptFunction; -import com.alphawallet.app.repository.TokenRepository; -import com.fasterxml.jackson.core.JsonParseException; - -import org.web3j.abi.FunctionEncoder; -import org.web3j.abi.FunctionReturnDecoder; -import org.web3j.abi.TypeReference; -import org.web3j.abi.datatypes.Address; -import org.web3j.abi.datatypes.Function; -import org.web3j.abi.datatypes.Type; -import org.web3j.abi.datatypes.Utf8String; -import org.web3j.crypto.Keys; -import org.web3j.ens.Contracts; -import org.web3j.ens.EnsResolutionException; -import org.web3j.ens.NameHash; -import org.web3j.protocol.Web3j; -import org.web3j.protocol.core.DefaultBlockParameterName; -import org.web3j.protocol.core.methods.response.EthBlock; -import org.web3j.protocol.core.methods.response.EthCall; -import org.web3j.protocol.core.methods.response.EthSyncing; -import org.web3j.protocol.core.methods.response.NetVersion; -import org.web3j.utils.Numeric; - -import java.io.InterruptedIOException; -import java.math.BigInteger; -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.List; - -import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; - -import timber.log.Timber; - -/** - * EnsResolver from Web3j adapted for Android Java's BigInteger - */ -public class EnsResolver -{ - - public static final long DEFAULT_SYNC_THRESHOLD = 1000 * 60 * 3; - public static final String REVERSE_NAME_SUFFIX = ".addr.reverse"; - public static final String CRYPTO_RESOLVER = "0xD1E5b0FF1287aA9f9A268759062E4Ab08b9Dacbe"; - public static final String CRYPTO_ETH_KEY = "crypto.ETH.address"; - - private final Web3j web3j; - private final int addressLength; - private long syncThreshold; // non-final in case this value needs to be tweaked - - public EnsNodeNotSyncCallback nodeNotSyncCallback = null; - - public EnsResolver(Web3j web3j, long syncThreshold, int addressLength) - { - this.web3j = web3j; - this.syncThreshold = syncThreshold; - this.addressLength = addressLength; - } - - public EnsResolver(Web3j web3j, long syncThreshold) - { - this(web3j, syncThreshold, Keys.ADDRESS_LENGTH_IN_HEX); - } - - public EnsResolver(Web3j web3j) - { - this(web3j, DEFAULT_SYNC_THRESHOLD); - } - - public void setSyncThreshold(long syncThreshold) - { - this.syncThreshold = syncThreshold; - } - - public long getSyncThreshold() - { - return syncThreshold; - } - - /** - * This function takes ensName (eg 'scotty.eth') and returns the matching Ethereum Address. - * NOTE: It is highly important to check the node is synced before resolving, as this could be an attack - * - * @param ensName - * @return - */ - public String resolve(String ensName, boolean performSync) - { - Timber.d("resolve %s, %s", ensName, performSync); - String contractAddress = ensName; - if (isValidEnsName(ensName, addressLength)) - { - try - { // performSync used to skip syncing if required by user - if (performSync && !isSynced()) //ensure node is synced - { - Timber.d("resolve: node not synced"); - if (nodeNotSyncCallback != null) - { - new Handler(Looper.getMainLooper()).post(() -> nodeNotSyncCallback.onNodeNotSynced()); - } - throw new EnsResolutionException("Node is not currently synced"); - } - else if (ensName.endsWith(".crypto")) //check crypto namespace - { - byte[] nameHash = NameHash.nameHashAsBytes(ensName); - BigInteger nameId = new BigInteger(nameHash); - String resolverAddress = getContractData(MAINNET_ID, CRYPTO_RESOLVER, getResolverOf(nameId)); - if (!TextUtils.isEmpty(resolverAddress)) - { - contractAddress = getContractData(MAINNET_ID, resolverAddress, get(nameId)); - } - } - else - { - String resolverAddress = lookupResolver(ensName); - if (!TextUtils.isEmpty(resolverAddress)) - { - byte[] nameHash = NameHash.nameHashAsBytes(ensName); - //now attempt to get the address of this ENS - contractAddress = getContractData(MAINNET_ID, resolverAddress, getAddr(nameHash)); - } - } - } - catch (Exception e) - { - //throw new RuntimeException("Unable to execute Ethereum request", e); - return ""; - } - - if (!Utils.isAddressValid(contractAddress)) - { - //throw new RuntimeException("Unable to resolve address for name: " + ensName); - return ""; - } - else - { - return contractAddress; - } - } - else - { - return ensName; - } - } - - /** - * Reverse name resolution as documented in the specification. - * - * @param address an ethereum address, example: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" - * @return a EnsName registered for provided address - */ - public String reverseResolve(String address) throws UnableToResolveENS - { - String name = ""; - if (Utils.isAddressValid(address)) - { - String reverseName = Numeric.cleanHexPrefix(address) + REVERSE_NAME_SUFFIX; - try - { - String resolverAddress = lookupResolver(reverseName); - byte[] nameHash = NameHash.nameHashAsBytes(reverseName); - name = getContractData(MAINNET_ID, resolverAddress, getName(nameHash)); - } - catch (Exception e) - { - //throw new RuntimeException("Unable to execute Ethereum request", e); - return ""; - } - - if (!isValidEnsName(name, addressLength)) - { - throw new UnableToResolveENS("Unable to resolve name for address: " + address); - } - else - { - return name; - } - } - else - { - throw new EnsResolutionException("Address is invalid: " + address); - } - } - - public String resolveAvatar(String ensName) - { - if (isValidEnsName(ensName, addressLength)) - { - try - { - String resolverAddress = lookupResolver(ensName); - if (!TextUtils.isEmpty(resolverAddress)) - { - byte[] nameHash = NameHash.nameHashAsBytes(ensName); - //now attempt to get the address of this ENS - return getContractData(MAINNET_ID, resolverAddress, getAvatar(nameHash)); - } - } - catch (Exception e) - { - // - Timber.e(e); - } - } - - return ""; - } - - public String resolveAvatarFromAddress(String address) - { - if (Utils.isAddressValid(address)) - { - String reverseName = Numeric.cleanHexPrefix(address.toLowerCase()) + REVERSE_NAME_SUFFIX; - try - { - String resolverAddress = lookupResolver(reverseName); - byte[] nameHash = NameHash.nameHashAsBytes(reverseName); - String avatar = getContractData(MAINNET_ID, resolverAddress, getAvatar(nameHash)); - return avatar != null ? avatar : ""; - } - catch (Exception e) - { - Timber.e(e); - //throw new RuntimeException("Unable to execute Ethereum request", e); - } - } - - return ""; - } - - private String lookupResolver(String ensName) throws Exception - { - NetVersion netVersion = web3j.netVersion().send(); - String registryContract = Contracts.resolveRegistryContract(netVersion.getNetVersion()); - byte[] nameHash = NameHash.nameHashAsBytes(ensName); - Function resolver = getResolver(nameHash); - return getContractData(MAINNET_ID, registryContract, resolver); - } - - private Function getResolver(byte[] nameHash) - { - return new Function("resolver", - Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)), - Arrays.asList(new TypeReference

() - { - })); - } - - private Function getAvatar(byte[] nameHash) - { - return new Function("text", - Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash), - new org.web3j.abi.datatypes.Utf8String("avatar")), - Arrays.asList(new TypeReference() - { - })); - } - - private Function getResolverOf(BigInteger nameId) - { - return new Function("resolverOf", - Arrays.asList(new org.web3j.abi.datatypes.Uint(nameId)), - Arrays.asList(new TypeReference
() - { - })); - } - - private Function get(BigInteger nameId) - { - return new Function("get", - Arrays.asList(new org.web3j.abi.datatypes.Utf8String(EnsResolver.CRYPTO_ETH_KEY), new org.web3j.abi.datatypes.generated.Uint256(nameId)), - Arrays.asList(new TypeReference() - { - })); - } - - private Function getAddr(byte[] nameHash) - { - return new Function("addr", - Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)), - Arrays.asList(new TypeReference
() - { - })); - } - - private Function getName(byte[] nameHash) - { - return new Function("name", - Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)), - Arrays.asList(new TypeReference() - { - })); - } - - boolean isSynced() throws Exception - { - EthSyncing ethSyncing = web3j.ethSyncing().send(); - if (ethSyncing.isSyncing()) - { - return false; - } - else - { - EthBlock ethBlock = - web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).send(); - long timestamp = ethBlock.getBlock().getTimestamp().longValue() * 1000; - - return System.currentTimeMillis() - syncThreshold < timestamp; - } - } - - private String callSmartContractFunction( - Function function, String contractAddress, long chainId) throws Exception - { - try - { - String encodedFunction = FunctionEncoder.encode(function); - - org.web3j.protocol.core.methods.request.Transaction transaction - = createEthCallTransaction(TokenscriptFunction.ZERO_ADDRESS, contractAddress, encodedFunction); - EthCall response = TokenRepository.getWeb3jService(chainId).ethCall(transaction, DefaultBlockParameterName.LATEST).send(); - - return response.getValue(); - } - catch (InterruptedIOException | UnknownHostException | JsonParseException e) - { - //expected to happen when user switches wallets - return "0x"; - } - } - - private T getContractData(long chainId, String address, Function function) throws Exception - { - String responseValue = callSmartContractFunction(function, address, chainId); - - if (TextUtils.isEmpty(responseValue)) - { - throw new Exception("Bad contract value"); - } - else if (responseValue.equals("0x")) - { - return (T) ""; - } - - List response = FunctionReturnDecoder.decode( - responseValue, function.getOutputParameters()); - if (response.size() == 1) - { - return (T) response.get(0).getValue(); - } - else - { - return (T) ""; - } - } - - public static boolean isValidEnsName(String input) - { - return isValidEnsName(input, Keys.ADDRESS_LENGTH_IN_HEX); - } - - public static boolean isValidEnsName(String input, int addressLength) - { - return input != null && input.contains(".") && input.length() > 4; - } -} diff --git a/app/src/main/java/com/alphawallet/app/util/LayoutHelper.java b/app/src/main/java/com/alphawallet/app/util/LayoutHelper.java new file mode 100644 index 0000000000..b07985887f --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/LayoutHelper.java @@ -0,0 +1,30 @@ +package com.alphawallet.app.util; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.ListView; + +public class LayoutHelper +{ + public static void resizeList(ListView listView) + { + ListAdapter listAdapter = listView.getAdapter(); + if (listAdapter == null) + { + return; + } + //set listAdapter in loop for getting final size + int totalHeight = 0; + for (int size = 0; size < listAdapter.getCount(); size++) + { + View listItem = listAdapter.getView(size, null, listView); + listItem.measure(0, 0); + totalHeight += listItem.getMeasuredHeight(); + } + //setting listview item in adapter + ViewGroup.LayoutParams params = listView.getLayoutParams(); + params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); + listView.setLayoutParams(params); + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/SwapUtils.java b/app/src/main/java/com/alphawallet/app/util/SwapUtils.java new file mode 100644 index 0000000000..724451cbbb --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/SwapUtils.java @@ -0,0 +1,83 @@ +package com.alphawallet.app.util; + +import com.alphawallet.app.entity.lifi.Action; +import com.alphawallet.app.entity.lifi.Estimate; +import com.alphawallet.app.entity.lifi.FeeCost; +import com.alphawallet.app.entity.lifi.GasCost; +import com.alphawallet.app.entity.lifi.Quote; + +import java.math.BigDecimal; +import java.util.ArrayList; + +public class SwapUtils +{ + private static final String CURRENT_PRICE_FORMAT = "1 %s ≈ %s %s"; + private static final String GAS_PRICE_FORMAT = "%s %s"; + private static final String FEE_FORMAT = "%s %s"; + private static final String MINIMUM_RECEIVED_FORMAT = "%s %s"; + + public static String getTotalGasFees(ArrayList gasCosts) + { + if (gasCosts != null) + { + StringBuilder gas = new StringBuilder(); + for (GasCost gc : gasCosts) + { + gas.append(SwapUtils.getGasFee(gc)).append(System.lineSeparator()); + } + return gas.toString().trim(); + } + else + { + return ""; + } + } + + public static String getGasFee(GasCost gasCost) + { + return String.format(GAS_PRICE_FORMAT, + BalanceUtils.getScaledValueFixed(new BigDecimal(gasCost.amount), gasCost.token.decimals, 4), + gasCost.token.symbol); + } + + public static String getOtherFees(ArrayList feeCosts) + { + if (feeCosts != null) + { + StringBuilder fees = new StringBuilder(); + for (FeeCost fc : feeCosts) + { + fees.append(fc.name); + fees.append(": "); + fees.append(SwapUtils.getFee(fc)).append(System.lineSeparator()); + } + return fees.toString().trim(); + } + else + { + return ""; + } + } + + public static String getFee(FeeCost feeCost) + { + return String.format(FEE_FORMAT, + BalanceUtils.getScaledValueFixed(new BigDecimal(feeCost.amount), feeCost.token.decimals, 4), + feeCost.token.symbol); + } + + public static String getFormattedCurrentPrice(Action action) + { + return String.format(CURRENT_PRICE_FORMAT, + action.fromToken.symbol, + action.getCurrentPrice(), + action.toToken.symbol); + } + + public static String getFormattedMinAmount(Estimate estimate, Action action) + { + return String.format(MINIMUM_RECEIVED_FORMAT, + BalanceUtils.getScaledValue(estimate.toAmountMin, action.toToken.decimals, 4), + action.toToken.symbol); + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index c096d2c3c2..8e9e071496 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -3,11 +3,11 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.CLASSIC_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.GNOSIS_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISTIC_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.POA_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_ID; import android.app.Activity; import android.content.Context; @@ -20,7 +20,6 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.StyleSpan; -import android.util.Patterns; import android.util.TypedValue; import android.webkit.URLUtil; @@ -31,6 +30,7 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.util.pattern.Patterns; import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.token.entity.ProviderTypedData; import com.alphawallet.token.entity.Signable; @@ -68,18 +68,19 @@ import timber.log.Timber; -public class Utils { - +public class Utils +{ private static final String ISOLATE_NUMERIC = "(0?x?[0-9a-fA-F]+)"; private static final String ICON_REPO_ADDRESS_TOKEN = "[TOKEN]"; private static final String CHAIN_REPO_ADDRESS_TOKEN = "[CHAIN]"; private static final String TOKEN_LOGO = "/logo.png"; - public static final String ALPHAWALLET_REPO_NAME = "https://raw.githubusercontent.com/alphawallet/iconassets/lowercased/"; + public static final String ALPHAWALLET_REPO_NAME = "https://raw.githubusercontent.com/alphawallet/iconassets/master/"; private static final String TRUST_ICON_REPO_BASE = "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/"; private static final String TRUST_ICON_REPO = TRUST_ICON_REPO_BASE + CHAIN_REPO_ADDRESS_TOKEN + "/assets/" + ICON_REPO_ADDRESS_TOKEN + TOKEN_LOGO; private static final String ALPHAWALLET_ICON_REPO = ALPHAWALLET_REPO_NAME + ICON_REPO_ADDRESS_TOKEN + TOKEN_LOGO; - public static int dp2px(Context context, int dp) { + public static int dp2px(Context context, int dp) + { Resources r = context.getResources(); return (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, @@ -88,22 +89,31 @@ public static int dp2px(Context context, int dp) { ); } - public static String formatUrl(String url) { - if (URLUtil.isHttpsUrl(url) || URLUtil.isHttpUrl(url)) { + public static String formatUrl(String url) + { + if (URLUtil.isHttpsUrl(url) || URLUtil.isHttpUrl(url)) + { return url; - } else { - if (isValidUrl(url)) { + } + else + { + if (isValidUrl(url)) + { return C.HTTPS_PREFIX + url; - } else { + } + else + { return C.INTERNET_SEARCH_PREFIX + url; } } } - public static boolean isValidUrl(String url) { + public static boolean isValidUrl(String url) + { + if (TextUtils.isEmpty(url)) return false; Pattern p = Patterns.WEB_URL; Matcher m = p.matcher(url.toLowerCase()); - return m.matches(); + return m.matches() || isIPFS(url); } public static boolean isAlNum(String testStr) @@ -146,7 +156,8 @@ public static boolean isValidValue(String testStr) return result; } - private static String getFirstWord(String text) { + private static String getFirstWord(String text) + { if (TextUtils.isEmpty(text)) return ""; text = text.trim(); int index; @@ -204,7 +215,7 @@ public static int getSigningTitle(Signable signable) return R.string.dialog_title_sign_personal_message; case SIGN_TYPED_DATA: case SIGN_TYPED_DATA_V3: - case SIGN_TYPES_DATA_V4: + case SIGN_TYPED_DATA_V4: return R.string.dialog_title_sign_typed_message; } } @@ -269,7 +280,7 @@ public static CharSequence createFormattedValue(Context ctx, String operationNam int spaceIndex = operationName.lastIndexOf(' '); if (spaceIndex > 0) { - operationName = operationName.substring(0, spaceIndex) + '\n' + operationName.substring(spaceIndex+1); + operationName = operationName.substring(0, spaceIndex) + '\n' + operationName.substring(spaceIndex + 1); } else { @@ -298,16 +309,20 @@ public static CharSequence createFormattedValue(Context ctx, String operationNam return sb; } - public static String loadJSONFromAsset(Context context, String fileName) { + public static String loadJSONFromAsset(Context context, String fileName) + { String json = null; - try { + try + { InputStream is = context.getAssets().open(fileName); int size = is.available(); byte[] buffer = new byte[size]; is.read(buffer); is.close(); json = new String(buffer, StandardCharsets.UTF_8); - } catch (IOException ex) { + } + catch (IOException ex) + { ex.printStackTrace(); return null; } @@ -374,7 +389,8 @@ public static List longListToArray(String list) public static int[] bigIntegerListToIntList(List ticketSendIndexList) { int[] indexList = new int[ticketSendIndexList.size()]; - for (int i = 0; i < ticketSendIndexList.size(); i++) indexList[i] = ticketSendIndexList.get(i).intValue(); + for (int i = 0; i < ticketSendIndexList.size(); i++) + indexList[i] = ticketSendIndexList.get(i).intValue(); return indexList; } @@ -395,6 +411,7 @@ public static BigInteger parseTokenId(String tokenIdStr) /** * Produce a string CSV of integer IDs given an input list of values + * * @param idList * @param keepZeros * @return @@ -453,7 +470,7 @@ public static String integerListToString(List intList, boolean keepZero for (Integer id : intList) { if (!keepZeros && id == 0) continue; - if (!first)sb.append(","); + if (!first) sb.append(","); sb.append(id); first = false; } @@ -478,7 +495,10 @@ public static boolean isNumeric(String numString) for (int i = 0; i < numString.length(); i++) { - if (Character.digit(numString.charAt(i), 10) == -1) { return false; } + if (Character.digit(numString.charAt(i), 10) == -1) + { + return false; + } } return true; @@ -491,7 +511,10 @@ public static boolean isHex(String hexStr) for (int i = 0; i < hexStr.length(); i++) { - if (Character.digit(hexStr.charAt(i), 16) == -1) { return false; } + if (Character.digit(hexStr.charAt(i), 16) == -1) + { + return false; + } } return true; @@ -518,7 +541,8 @@ public static String isolateNumeric(String valueFromInput) return valueFromInput; } - public static String formatAddress(String address) { + public static String formatAddress(String address) + { if (isAddressValid(address)) { address = Keys.toChecksumAddress(address); @@ -535,12 +559,15 @@ public static String formatAddress(String address) { /** * Just enough for diagnosis of most errors + * * @param s String to be HTML escaped * @return escaped string */ - public static String escapeHTML(String s) { + public static String escapeHTML(String s) + { StringBuilder out = new StringBuilder(Math.max(16, s.length())); - for (int i = 0; i < s.length(); i++) { + for (int i = 0; i < s.length(); i++) + { char c = s.charAt(i); switch (c) { @@ -565,12 +592,12 @@ public static String escapeHTML(String s) { public static String convertTimePeriodInSeconds(long pendingTimeInSeconds, Context ctx) { - long days = pendingTimeInSeconds/(60*60*24); - pendingTimeInSeconds -= (days*60*60*24); - long hours = pendingTimeInSeconds/(60*60); - pendingTimeInSeconds -= (hours*60*60); - long minutes = pendingTimeInSeconds/60; - long seconds = pendingTimeInSeconds%60; + long days = pendingTimeInSeconds / (60 * 60 * 24); + pendingTimeInSeconds -= (days * 60 * 60 * 24); + long hours = pendingTimeInSeconds / (60 * 60); + pendingTimeInSeconds -= (hours * 60 * 60); + long minutes = pendingTimeInSeconds / 60; + long seconds = pendingTimeInSeconds % 60; StringBuilder sb = new StringBuilder(); int timePoints = 0; @@ -647,12 +674,12 @@ public static String convertTimePeriodInSeconds(long pendingTimeInSeconds, Conte public static String shortConvertTimePeriodInSeconds(long pendingTimeInSeconds, Context ctx) { - long days = pendingTimeInSeconds/(60*60*24); - pendingTimeInSeconds -= (days*60*60*24); - long hours = pendingTimeInSeconds/(60*60); - pendingTimeInSeconds -= (hours*60*60); - long minutes = pendingTimeInSeconds/60; - long seconds = pendingTimeInSeconds%60; + long days = pendingTimeInSeconds / (60 * 60 * 24); + pendingTimeInSeconds -= (days * 60 * 60 * 24); + long hours = pendingTimeInSeconds / (60 * 60); + pendingTimeInSeconds -= (hours * 60 * 60); + long minutes = pendingTimeInSeconds / 60; + long seconds = pendingTimeInSeconds % 60; String timeStr; @@ -672,7 +699,7 @@ else if (hours > 0) } else { - BigDecimal hourStr = BigDecimal.valueOf(hours + (double)minutes/60.0) + BigDecimal hourStr = BigDecimal.valueOf(hours + (double) minutes / 60.0) .setScale(1, RoundingMode.HALF_DOWN); //to 1 dp timeStr = ctx.getString(R.string.hour_plural, hourStr.toString()); } @@ -685,7 +712,7 @@ else if (minutes > 0) } else { - BigDecimal minsStr = BigDecimal.valueOf(minutes + (double)seconds/60.0) + BigDecimal minsStr = BigDecimal.valueOf(minutes + (double) seconds / 60.0) .setScale(1, RoundingMode.HALF_DOWN); //to 1 dp timeStr = ctx.getString(R.string.minute_plural, minsStr.toString()); } @@ -720,7 +747,8 @@ public static String localiseUnixDate(Context ctx, long timeStampInSec) return timeFormat.format(date) + " | " + dateFormat.format(date); } - public static long randomId() { + public static long randomId() + { return new Date().getTime(); } @@ -770,15 +798,16 @@ public static String getTokenAddrFromAWUrl(String url) return ""; } - private static final Map twChainNames = new HashMap() { + private static final Map twChainNames = new HashMap() + { { put(CLASSIC_ID, "classic"); - put(XDAI_ID, "xdai"); + put(GNOSIS_ID, "xdai"); put(POA_ID, "poa"); put(BINANCE_MAIN_ID, "smartchain"); put(AVALANCHE_ID, "avalanche"); put(OPTIMISTIC_MAIN_ID, "optimism"); - put(MATIC_ID, "polygon"); + put(POLYGON_ID, "polygon"); put(MAINNET_ID, "ethereum"); } }; @@ -805,37 +834,74 @@ public static boolean isContractCall(Context context, String operationName) } private static final String IPFS_PREFIX = "ipfs://"; + private static final String IPFS_DESIGNATOR = "/ipfs/"; + public static final String IPFS_INFURA_RESOLVER = "https://alphawallet.infura-ipfs.io"; + public static final String IPFS_IO_RESOLVER = "https://ipfs.io"; + + public static boolean isIPFS(String url) + { + return url.contains(IPFS_DESIGNATOR) || url.startsWith(IPFS_PREFIX) || shouldBeIPFS(url); + } public static String parseIPFS(String URL) + { + return resolveIPFS(URL, IPFS_INFURA_RESOLVER); + } + + public static String resolveIPFS(String URL, String resolver) { if (TextUtils.isEmpty(URL)) return URL; String parsed = URL; - int ipfsIndex = URL.lastIndexOf("/ipfs/"); + int ipfsIndex = URL.lastIndexOf(IPFS_DESIGNATOR); if (ipfsIndex >= 0) { - parsed = "https://gateway.ipfs.io" + URL.substring(ipfsIndex); + parsed = resolver + URL.substring(ipfsIndex); } else if (URL.startsWith(IPFS_PREFIX)) { - parsed = "https://gateway.ipfs.io/ipfs/" + URL.substring(IPFS_PREFIX.length()); + parsed = resolver + IPFS_DESIGNATOR + URL.substring(IPFS_PREFIX.length()); + } + else if (shouldBeIPFS(URL)) //have seen some NFTs designating only the IPFS hash + { + parsed = resolver + IPFS_DESIGNATOR + URL; } return parsed; } - public static String loadFile(Context context, @RawRes int rawRes) { + private static boolean shouldBeIPFS(String url) + { + return url.startsWith("Qm") && url.length() == 46 && !url.contains(".") && !url.contains("/"); + } + + public static String loadFile(Context context, @RawRes int rawRes) + { byte[] buffer = new byte[0]; - try { + try + { InputStream in = context.getResources().openRawResource(rawRes); buffer = new byte[in.available()]; int len = in.read(buffer); - if (len < 1) { + if (len < 1) + { throw new IOException("Nothing is read."); } - } catch (Exception ex) { + } + catch (Exception ex) + { Timber.tag("READ_JS_TAG").d(ex, "Ex"); } - return new String(buffer); + + try + { + Timber.tag("READ_JS_TAG").d("HeapSize:%s", Runtime.getRuntime().freeMemory()); + return new String(buffer); + } + catch (Exception e) + { + Timber.tag("READ_JS_TAG").d(e, "Ex"); + } + return ""; } public static long timeUntil(long eventInMillis) @@ -844,12 +910,14 @@ public static long timeUntil(long eventInMillis) } //TODO: detect various App Library installs and re-direct appropriately - public static boolean verifyInstallerId(Context context) { + public static boolean verifyInstallerId(Context context) + { try { PackageManager packageManager = context.getPackageManager(); String installingPackageName; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + { final InstallSourceInfo installer = packageManager.getInstallSourceInfo(context.getPackageName()); installingPackageName = installer.getInstallingPackageName(); } @@ -874,16 +942,20 @@ public static boolean isTransactionHash(String input) if (input == null || (input.length() != 66 && input.length() != 64)) return false; String cleanInput = Numeric.cleanHexPrefix(input); - try { + try + { Numeric.toBigIntNoPrefix(cleanInput); - } catch (NumberFormatException e) { + } + catch (NumberFormatException e) + { return false; } return cleanInput.length() == 64; } - public static @ColorInt int getColorFromAttr(Context context, int resId) + public static @ColorInt + int getColorFromAttr(Context context, int resId) { TypedValue typedValue = new TypedValue(); Resources.Theme theme = context.getTheme(); @@ -962,4 +1034,9 @@ else if (context instanceof ContextWrapper) return false; } } + + public static String removeDoubleQuotes(String string) + { + return string != null ? string.replace("\"", "") : null; + } } diff --git a/app/src/main/java/com/alphawallet/app/util/Wallets.java b/app/src/main/java/com/alphawallet/app/util/Wallets.java new file mode 100644 index 0000000000..eef15d2d77 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/Wallets.java @@ -0,0 +1,20 @@ +package com.alphawallet.app.util; + +import com.alphawallet.app.entity.Wallet; + +import java.util.ArrayList; + +public class Wallets +{ + public static Wallet[] filter(Wallet[] wallets) + { + ArrayList list = new ArrayList<>(); + for (Wallet w : wallets) + { + if (!w.watchOnly()) + list.add(w); + } + + return list.toArray(new Wallet[0]); + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/AWEnsResolver.java b/app/src/main/java/com/alphawallet/app/util/ens/AWEnsResolver.java similarity index 72% rename from app/src/main/java/com/alphawallet/app/util/AWEnsResolver.java rename to app/src/main/java/com/alphawallet/app/util/ens/AWEnsResolver.java index 031c2e800b..db848220f7 100644 --- a/app/src/main/java/com/alphawallet/app/util/AWEnsResolver.java +++ b/app/src/main/java/com/alphawallet/app/util/ens/AWEnsResolver.java @@ -1,4 +1,4 @@ -package com.alphawallet.app.util; +package com.alphawallet.app.util.ens; import android.content.Context; import android.text.TextUtils; @@ -8,17 +8,17 @@ import com.alphawallet.app.C; import com.alphawallet.app.entity.UnableToResolveENS; import com.alphawallet.app.service.OpenSeaService; -import com.alphawallet.app.util.das.DASBody; -import com.alphawallet.app.util.das.DASRecord; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.web3j.ens.EnsResolutionException; import com.alphawallet.token.tools.Numeric; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.json.JSONObject; import org.web3j.protocol.Web3j; -import org.web3j.protocol.http.HttpService; import java.util.HashMap; +import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; @@ -27,37 +27,48 @@ import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; import timber.log.Timber; /** * Created by James on 29/05/2019. * Stormbird in Sydney */ -public class AWEnsResolver extends EnsResolver +public class AWEnsResolver { - private static final long DEFAULT_SYNC_THRESHOLD = 1000 * 60 * 3; - private static final String DAS_LOOKUP = "https://indexer.da.systems/"; - private static final String DAS_NAME = "[DAS_NAME]"; - private static final String DAS_PAYLOAD = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"das_searchAccount\",\"params\":[\"" + DAS_NAME + "\"]}"; private static final String OPENSEA_IMAGE_PREVIEW = "image_preview_url"; private static final String OPENSEA_IMAGE_ORIGINAL = "image_original_url"; //in case of SVG; Opensea breaks SVG compression private final Context context; private final OkHttpClient client; + private HashMap resolvables; + private final EnsResolver ensResolver; - static + public AWEnsResolver(Web3j web3j, Context context) { - System.loadLibrary("keys"); + this(web3j, context, -1); } - public static native String getOpenSeaKey(); - - public AWEnsResolver(Web3j web3j, Context context) + public AWEnsResolver(Web3j web3j, Context context, long chainId) { - super(web3j, DEFAULT_SYNC_THRESHOLD); + this.ensResolver = new EnsResolver(web3j); this.context = context; this.client = setupClient(); + + resolvables = new HashMap<>() + { + { + put(".bit", new DASResolver(client)); +// put(".crypto", new CryptoResolver(ensResolver)); + put(".crypto", new UnstoppableDomainsResolver(client, chainId)); + put(".zil", new UnstoppableDomainsResolver(client, chainId)); + put(".wallet", new UnstoppableDomainsResolver(client, chainId)); + put(".x", new UnstoppableDomainsResolver(client, chainId)); + put(".nft", new UnstoppableDomainsResolver(client, chainId)); + put(".888", new UnstoppableDomainsResolver(client, chainId)); + put(".dao", new UnstoppableDomainsResolver(client, chainId)); + put(".blockchain", new UnstoppableDomainsResolver(client, chainId)); + put(".bitcoin", new UnstoppableDomainsResolver(client, chainId)); + } + }; } /** @@ -74,11 +85,11 @@ public Single reverseResolveEns(String address) try { - ensName = reverseResolve(address); //no known ENS for this address, resolve from reverse resolver + ensName = ensResolver.reverseResolve(address); //no known ENS for this address, resolve from reverse resolver if (!TextUtils.isEmpty(ensName)) { //check ENS name integrity - it must point to the wallet address - String resolveAddress = resolve(ensName, true); + String resolveAddress = resolve(ensName); if (!resolveAddress.equalsIgnoreCase(address)) { ensName = ""; @@ -89,6 +100,10 @@ public Single reverseResolveEns(String address) { ensName = fetchPreviouslyUsedENS(address); } + catch (EnsResolutionException e) + { + // Expected to throw when ENS name invalid + } catch (Exception e) { Timber.e(e); @@ -213,6 +228,7 @@ private enum LocatorType public String checkENSHistoryForAddress(String address) { String ensName = ""; + if (context == null) return ensName; //try previously resolved names String historyJson = PreferenceManager.getDefaultSharedPreferences(context).getString(C.ENS_HISTORY_PAIR, ""); if (historyJson.length() > 0) @@ -220,9 +236,9 @@ public String checkENSHistoryForAddress(String address) HashMap history = new Gson().fromJson(historyJson, new TypeToken>() { }.getType()); - if (history.containsKey(address.toLowerCase())) + if (history.containsKey(address.toLowerCase(Locale.ENGLISH))) { - ensName = history.get(address.toLowerCase()); + ensName = history.get(address.toLowerCase(Locale.ENGLISH)); } } @@ -232,6 +248,7 @@ public String checkENSHistoryForAddress(String address) private String fetchPreviouslyUsedENS(String address) { String ensName = ""; + if (context == null) return ensName; //try previously resolved names String historyJson = PreferenceManager.getDefaultSharedPreferences(context).getString(C.ENS_HISTORY_PAIR, ""); if (historyJson.length() > 0) @@ -239,11 +256,11 @@ private String fetchPreviouslyUsedENS(String address) HashMap history = new Gson().fromJson(historyJson, new TypeToken>() { }.getType()); - if (history.containsKey(address.toLowerCase())) + if (history.containsKey(address.toLowerCase(Locale.ENGLISH))) { - String previouslyUsedDomain = history.get(address.toLowerCase()); + String previouslyUsedDomain = history.get(address.toLowerCase(Locale.ENGLISH)); //perform an additional check, to ensure this ENS name is still valid, try this ENS name to see if it resolves to the address - ensName = resolveENSAddress(previouslyUsedDomain, true) + ensName = resolveENSAddress(previouslyUsedDomain) .map(resolvedAddress -> checkResolvedAddressMatches(resolvedAddress, address, previouslyUsedDomain)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) @@ -272,72 +289,65 @@ private String checkResolvedAddressMatches(String resolvedAddress, String addres * @param ensName ensName to be resolved to address * @return Ethereum address or empty string */ - public Single resolveENSAddress(String ensName, boolean performNodeSync) + public Single resolveENSAddress(String ensName) { return Single.fromCallable(() -> { - Timber.d("Verify: " + ensName); + Timber.d("Verify: %s", ensName); String address = ""; - if (!isValidEnsName(ensName)) return ""; + if (!EnsResolver.isValidEnsName(ensName)) return ""; try { - address = resolve(ensName, performNodeSync); + address = resolve(ensName); } catch (Exception e) { - Timber.d("Verify: error: " + e.getMessage()); + Timber.d("Verify: error: %s", e.getMessage()); // no action } return address; }).onErrorReturnItem(""); } - @Override - public String resolve(String ensName, boolean performSync) + public String resolve(String ensName) throws Exception { - if (!TextUtils.isEmpty(ensName) && ensName.endsWith(".bit")) + if (TextUtils.isEmpty(ensName)) { - return resolveDAS(ensName); + return ""; } - else + + Resolvable resolvable = resolvables.get(suffixOf(ensName)); + if (resolvable == null) { - return super.resolve(ensName, performSync); + resolvable = ensResolver; } + return resolvable.resolve(ensName); } - private String resolveDAS(String ensName) + private String suffixOf(String ensName) { - String payload = DAS_PAYLOAD.replace(DAS_NAME, ensName); + return ensName.substring(ensName.lastIndexOf(".")); + } - RequestBody requestBody = RequestBody.create(payload, HttpService.JSON_MEDIA_TYPE); - Request request = new Request.Builder() - .url(DAS_LOOKUP) - .post(requestBody) - .build(); + public String resolveAvatar(String ensName) + { + return new AvatarResolver(ensResolver).resolve(ensName); + } - try (okhttp3.Response response = client.newCall(request).execute()) + public String resolveAvatarFromAddress(String address) + { + if (Utils.isAddressValid(address)) { - //get result - String result = response.body() != null ? response.body().string() : ""; - - DASBody dasResult = new Gson().fromJson(result, DASBody.class); - dasResult.buildMap(); - - //find ethereum entry - DASRecord ethLookup = dasResult.records.get("address.eth"); - if (ethLookup != null) + try { - return ethLookup.getAddress(); + String ensName = ensResolver.reverseResolve(address); + return resolveAvatar(ensName); } - else + catch (Exception e) { - return dasResult.getEthOwner(); + Timber.e(e); } } - catch (Exception e) - { - Timber.tag("ENS").d(e.getMessage()); - } return ""; } diff --git a/app/src/main/java/com/alphawallet/app/util/ens/AvatarResolver.java b/app/src/main/java/com/alphawallet/app/util/ens/AvatarResolver.java new file mode 100644 index 0000000000..898d7a2919 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ens/AvatarResolver.java @@ -0,0 +1,56 @@ +package com.alphawallet.app.util.ens; + +import android.text.TextUtils; + +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.ens.NameHash; + +import java.util.Arrays; + +import timber.log.Timber; + +public class AvatarResolver implements Resolvable +{ + private final EnsResolver ensResolver; + + public AvatarResolver(EnsResolver ensResolver) + { + this.ensResolver = ensResolver; + } + + public String resolve(String ensName) + { + if (ensResolver.validate(ensName)) + { + try + { + String resolverAddress = ensResolver.getResolverAddress(ensName); + if (!TextUtils.isEmpty(resolverAddress)) + { + byte[] nameHash = NameHash.nameHashAsBytes(ensName); + //now attempt to get the address of this ENS + return ensResolver.getContractData(resolverAddress, getAvatar(nameHash), ""); + } + } + catch (Exception e) + { + // + Timber.e(e); + } + } + + return ""; + } + + private Function getAvatar(byte[] nameHash) + { + return new Function("text", + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash), + new org.web3j.abi.datatypes.Utf8String("avatar")), + Arrays.asList(new TypeReference() + { + })); + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/ens/CryptoResolver.java b/app/src/main/java/com/alphawallet/app/util/ens/CryptoResolver.java new file mode 100644 index 0000000000..1f9adc4d5b --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ens/CryptoResolver.java @@ -0,0 +1,58 @@ +package com.alphawallet.app.util.ens; + +import android.text.TextUtils; + +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.ens.NameHash; + +import java.math.BigInteger; +import java.util.Arrays; + +public class CryptoResolver implements Resolvable +{ + private static final String CRYPTO_RESOLVER = "0xD1E5b0FF1287aA9f9A268759062E4Ab08b9Dacbe"; + private static final String CRYPTO_ETH_KEY = "crypto.ETH.address"; + + private final EnsResolver ensResolver; + + public CryptoResolver(EnsResolver ensResolver) + { + this.ensResolver = ensResolver; + } + + public String resolve(String ensName) throws Exception + { + byte[] nameHash = NameHash.nameHashAsBytes(ensName); + BigInteger nameId = new BigInteger(nameHash); + String resolverAddress = ensResolver.getContractData(CRYPTO_RESOLVER, getResolverOf(nameId), ""); + if (!TextUtils.isEmpty(resolverAddress)) + { + return ensResolver.getContractData(resolverAddress, get(nameId), ""); + } + else + { + return ""; + } + } + + private Function get(BigInteger nameId) + { + return new Function("get", + Arrays.asList(new org.web3j.abi.datatypes.Utf8String(CRYPTO_ETH_KEY), new org.web3j.abi.datatypes.generated.Uint256(nameId)), + Arrays.asList(new TypeReference() + { + })); + } + + private Function getResolverOf(BigInteger nameId) + { + return new Function("resolverOf", + Arrays.asList(new org.web3j.abi.datatypes.Uint(nameId)), + Arrays.asList(new TypeReference
() + { + })); + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/ens/DASResolver.java b/app/src/main/java/com/alphawallet/app/util/ens/DASResolver.java new file mode 100644 index 0000000000..a31c100099 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ens/DASResolver.java @@ -0,0 +1,62 @@ +package com.alphawallet.app.util.ens; + +import com.alphawallet.app.util.das.DASBody; +import com.alphawallet.app.util.das.DASRecord; +import com.google.gson.Gson; + +import org.web3j.protocol.http.HttpService; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import timber.log.Timber; + +public class DASResolver implements Resolvable +{ + private static final String DAS_LOOKUP = "https://indexer.da.systems/"; + private static final String DAS_NAME = "[DAS_NAME]"; + private static final String DAS_PAYLOAD = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"das_searchAccount\",\"params\":[\"" + DAS_NAME + "\"]}"; + private final OkHttpClient client; + + public DASResolver(OkHttpClient client) + { + this.client = client; + } + + public String resolve(String name) + { + String payload = DAS_PAYLOAD.replace(DAS_NAME, name); + + RequestBody requestBody = RequestBody.create(payload, HttpService.JSON_MEDIA_TYPE); + Request request = new Request.Builder() + .url(DAS_LOOKUP) + .post(requestBody) + .build(); + + try (okhttp3.Response response = client.newCall(request).execute()) + { + //get result + String result = response.body() != null ? response.body().string() : ""; + + DASBody dasResult = new Gson().fromJson(result, DASBody.class); + dasResult.buildMap(); + + //find ethereum entry + DASRecord ethLookup = dasResult.records.get("address.eth"); + if (ethLookup != null) + { + return ethLookup.getAddress(); + } + else + { + return dasResult.getEthOwner(); + } + } + catch (Exception e) + { + Timber.tag("ENS").e(e); + } + + return ""; + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/ens/EnsResolver.java b/app/src/main/java/com/alphawallet/app/util/ens/EnsResolver.java new file mode 100644 index 0000000000..e17abc12ff --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ens/EnsResolver.java @@ -0,0 +1,611 @@ +/* + * Copyright 2019 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.util.ens; + +import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; + +import android.text.TextUtils; + +import com.alphawallet.app.entity.tokenscript.TokenscriptFunction; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.web3j.ens.Contracts; +import com.alphawallet.app.web3j.ens.EnsGatewayRequestDTO; +import com.alphawallet.app.web3j.ens.EnsGatewayResponseDTO; +import com.alphawallet.app.web3j.ens.EnsResolutionException; +import com.alphawallet.app.web3j.ens.EnsUtils; +import com.alphawallet.app.web3j.ens.NameHash; +import com.alphawallet.app.web3j.ens.OffchainLookup; +import com.alphawallet.token.entity.ContractAddress; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.abi.DefaultFunctionReturnDecoder; +import org.web3j.abi.FunctionEncoder; +import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Bool; +import org.web3j.abi.datatypes.DynamicBytes; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.crypto.Keys; +import org.web3j.crypto.WalletUtils; +import org.web3j.protocol.ObjectMapperFactory; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.response.EthCall; +import org.web3j.protocol.core.methods.response.NetVersion; +import org.web3j.utils.Numeric; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import timber.log.Timber; + +/** Resolution logic for contract addresses. According to https://eips.ethereum.org/EIPS/eip-2544 */ +public class EnsResolver implements Resolvable +{ + + private static final Logger log = LoggerFactory.getLogger(EnsResolver.class); + + public static final MediaType JSON = MediaType.parse("application/json"); + + // Permit number offchain calls for a single contract call. + public static final int LOOKUP_LIMIT = 4; + private static final long ENS_CACHE_TIME_VALIDITY = 10 * (1000*60); //10 minutes + + public static final String REVERSE_NAME_SUFFIX = ".addr.reverse"; + + private final Web3j web3j; + protected final int addressLength; + protected long chainId; + + private OkHttpClient client = new OkHttpClient(); + + private static DefaultFunctionReturnDecoder decoder; + + public EnsResolver(Web3j web3j, int addressLength) + { + this.web3j = web3j; + this.addressLength = addressLength; + + chainId = 1; + + Single.fromCallable(() -> { + NetVersion v = web3j.netVersion().send(); + String ver = v.getNetVersion(); + return Long.parseLong(ver); + }).subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(id -> this.chainId = id, Timber::w) + .isDisposed(); + } + + public EnsResolver(Web3j web3j) { + this(web3j, Keys.ADDRESS_LENGTH_IN_HEX); + } + + protected ContractAddress obtainOffChainResolverAddress(String ensName) throws Exception + { + return new ContractAddress(chainId, getResolverAddress(ensName)); + } + + private static class CachedENSRead + { + public final String cachedResult; + public final long cachedResultTime; + + public CachedENSRead(String result) + { + cachedResult = result; + cachedResultTime = System.currentTimeMillis(); + } + + public boolean isValid() + { + return System.currentTimeMillis() < (cachedResultTime + ENS_CACHE_TIME_VALIDITY); //10 minutes cache validity + } + } + + //Need to cache results for Resolve + private static final Map cachedNameReads = new ConcurrentHashMap<>(); + + private String cacheKey(String ensName, String addrFunction) + { + return ((ensName != null) ? ensName : "") + "%" + ((addrFunction != null) ? addrFunction : ""); + } + + private String resolveWithCaching(String ensName, byte[] nameHash, String resolverAddr) throws Exception + { + String dnsEncoded = NameHash.dnsEncode(ensName); + String addrFunction = encodeResolverAddr(nameHash); + + CachedENSRead lookupData = cachedNameReads.get(cacheKey(ensName, addrFunction)); + String lookupDataHex = lookupData != null ? lookupData.cachedResult : null; + + if (lookupData == null || !lookupData.isValid()) + { + EthCall result = + resolve( + Numeric.hexStringToByteArray(dnsEncoded), + Numeric.hexStringToByteArray(addrFunction), + resolverAddr); + lookupDataHex = result.isReverted() ? Utils.removeDoubleQuotes(result.getError().getData()) : result.getValue();// .toString(); + if (!TextUtils.isEmpty(lookupDataHex) && !lookupDataHex.equals("0x")) + { + cachedNameReads.put(cacheKey(ensName, addrFunction), new CachedENSRead(lookupDataHex)); + } + } + + return lookupDataHex; + } + + /** + * Returns the address of the resolver for the specified node. + * + * @param ensName The specified node. + * @return address of the resolver. + */ + @Override + public String resolve(String ensName) throws Exception + { + if (TextUtils.isEmpty(ensName) || (ensName.trim().length() == 1 && ensName.contains("."))) { + return null; + } + + try { + if (isValidEnsName(ensName, addressLength)) + { + ContractAddress resolverAddress = obtainOffChainResolverAddress(ensName); + + boolean supportWildcard = + supportsInterface(EnsUtils.ENSIP_10_INTERFACE_ID, resolverAddress.address); + byte[] nameHash = NameHash.nameHashAsBytes(ensName); + + String resolvedName; + if (supportWildcard) { + String lookupDataHex = resolveWithCaching(ensName, nameHash, resolverAddress.address); + resolvedName = resolveOffchain(lookupDataHex, resolverAddress, LOOKUP_LIMIT); + } else { + try { + resolvedName = resolverAddr(nameHash, resolverAddress.address); + } catch (Exception e) { + throw new RuntimeException("Unable to execute Ethereum request: ", e); + } + } + + if (!WalletUtils.isValidAddress(resolvedName)) { + throw new EnsResolutionException( + "Unable to resolve address for name: " + ensName); + } else { + return resolvedName; + } + + } else { + return ensName; + } + } catch (Exception e) { + throw new EnsResolutionException(e); + } + } + + protected String resolveOffchain( + String lookupData, ContractAddress resolverAddress, int lookupCounter) + throws Exception + { + if (EnsUtils.isEIP3668(lookupData)) + { + OffchainLookup offchainLookup = + OffchainLookup.build(Numeric.hexStringToByteArray(lookupData.substring(10))); + + if (!resolverAddress.address.equals(offchainLookup.getSender())) + { + throw new EnsResolutionException( + "Cannot handle OffchainLookup raised inside nested call"); + } + + String gatewayResult = + ccipReadFetch( + offchainLookup.getUrls(), + offchainLookup.getSender(), + Numeric.toHexString(offchainLookup.getCallData())); + + if (gatewayResult == null) + { + throw new EnsResolutionException("CCIP Read disabled or provided no URLs."); + } + + ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(); + EnsGatewayResponseDTO gatewayResponseDTO = + objectMapper.readValue(gatewayResult, EnsGatewayResponseDTO.class); + + EthCall result = + resolveWithProof( + Numeric.hexStringToByteArray(gatewayResponseDTO.getData()), + offchainLookup.getExtraData(), resolverAddress.address); + + String resolvedNameHex = result.isReverted() ? Utils.removeDoubleQuotes(result.getError().getData()) : result.getValue();// .toString(); + + // This protocol can result in multiple lookups being requested by the same contract. + if (EnsUtils.isEIP3668(resolvedNameHex)) + { + if (lookupCounter <= 0) + { + throw new EnsResolutionException("Lookup calls is out of limit."); + } + + return resolveOffchain(lookupData, resolverAddress, --lookupCounter); + } + else + { + byte[] resolvedNameBytes = decodeDynamicBytes(resolvedNameHex); + + return decodeAddress( + Numeric.toHexString(resolvedNameBytes)); + } + } + + return lookupData; + } + + private static byte[] decodeDynamicBytes(String rawInput) + { + if (decoder == null) decoder = new DefaultFunctionReturnDecoder(); + List outputParameters = new ArrayList>(); + outputParameters.add(new TypeReference() {}); + + List typeList = decoder.decodeFunctionResult(rawInput, outputParameters); + + return typeList.isEmpty() ? null : ((DynamicBytes) typeList.get(0)).getValue(); + } + + private static String decodeAddress(String rawInput) + { + if (decoder == null) decoder = new DefaultFunctionReturnDecoder(); + List outputParameters = new ArrayList>(); + outputParameters.add(new TypeReference
() {}); + + List typeList = decoder.decodeFunctionResult(rawInput, outputParameters); + + return typeList.isEmpty() ? null : ((Address) typeList.get(0)).getValue(); + } + + protected String ccipReadFetch(List urls, String sender, String data) { + List errorMessages = new ArrayList<>(); + + for (String url : urls) { + Request request; + try { + request = buildRequest(url, sender, data); + } catch (JsonProcessingException | EnsResolutionException e) { + log.error(e.getMessage(), e); + break; + } + + try (okhttp3.Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody == null) { + log.warn("Response body is null, url: {}", url); + break; + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody.byteStream()))) + { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } + catch (Exception e) + { + // + return ""; + } + } else { + int statusCode = response.code(); + // 4xx indicates the result is not present; stop + if (statusCode >= 400 && statusCode < 500) { + log.error( + "Response error during CCIP fetch: url {}, error: {}", + url, + response.message()); + throw new EnsResolutionException(response.message()); + } + + // 5xx indicates server issue; try the next url + errorMessages.add(response.message()); + + log.warn( + "Response error 500 during CCIP fetch: url {}, error: {}", + url, + response.message()); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + log.warn(Arrays.toString(errorMessages.toArray())); + return null; + } + + protected Request buildRequest(String url, String sender, String data) + throws JsonProcessingException { + if (sender == null || !WalletUtils.isValidAddress(sender)) { + throw new EnsResolutionException("Sender address is null or not valid"); + } + if (data == null) { + throw new EnsResolutionException("Data is null"); + } + if (!url.contains("{sender}")) { + throw new EnsResolutionException("Url is not valid, sender parameter is not exist"); + } + + // URL expansion + String href = url.replace("{sender}", sender).replace("{data}", data); + + Request.Builder builder = new Request.Builder().url(href); + + if (url.contains("{data}")) { + return builder.get().build(); + } else { + EnsGatewayRequestDTO requestDTO = new EnsGatewayRequestDTO(data); + ObjectMapper om = ObjectMapperFactory.getObjectMapper(); + + return builder.post(RequestBody.create(om.writeValueAsString(requestDTO), JSON)) + .addHeader("Content-Type", "application/json") + .build(); + } + } + + /** + * Reverse name resolution as documented in the specification. + * + * @param address an ethereum address, example: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" + * @return a EnsName registered for provided address + */ + public String reverseResolve(String address) throws Exception + { + if (WalletUtils.isValidAddress(address, addressLength)) + { + String reverseName = Numeric.cleanHexPrefix(address) + REVERSE_NAME_SUFFIX; + ContractAddress resolverAddress = obtainOffChainResolverAddress(reverseName); + + byte[] nameHash = NameHash.nameHashAsBytes(reverseName); + String name; + try { + name = resolveName(nameHash, resolverAddress.address); + } catch (Exception e) { + throw new RuntimeException("Unable to execute Ethereum request", e); + } + + if (!isValidEnsName(name, addressLength)) { + throw new RuntimeException("Unable to resolve name for address: " + address); + } else { + return name; + } + } else { + throw new EnsResolutionException("Address is invalid: " + address); + } + } + + private Function getResolver(byte[] nameHash) + { + return new Function("resolver", + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(nameHash)), + Arrays.asList(new TypeReference
() + { + })); + } + + public String getResolverAddress(String ensName) throws Exception + { + String registryContract = Contracts.resolveRegistryContract(chainId); + byte[] nameHash = NameHash.nameHashAsBytes(ensName); + Function resolver = getResolver(nameHash); + String address = getContractData(registryContract, resolver, ""); + + if (EnsUtils.isAddressEmpty(address)) { + address = getResolverAddress(EnsUtils.getParent(ensName)); + } + + return address; + } + + public boolean validate(String input) { + return isValidEnsName(input, addressLength); + } + + public static boolean isValidEnsName(String input) { + return isValidEnsName(input, Keys.ADDRESS_LENGTH_IN_HEX); + } + + public static boolean isValidEnsName(String input, int addressLength) { + return input != null // will be set to null on new Contract creation + && (input.contains(".") || !WalletUtils.isValidAddress(input, addressLength)); + } + + public void setHttpClient(OkHttpClient client) { + this.client = client; + } + + public static final String FUNC_SUPPORTSINTERFACE = "supportsInterface"; + public static final String FUNC_addr = "addr"; + public static final String FUNC_RESOLVE = "resolve"; + public static final String FUNC_RESOLVEWITHPROOF = "resolveWithProof"; + public static final String FUNC_NAME = "name"; + + public boolean supportsInterface(byte[] interfaceID, String address) throws Exception + { + final org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(FUNC_SUPPORTSINTERFACE, + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes4(interfaceID)), + Arrays.asList(new TypeReference() + { + })); + + return getContractData(address, function, true); + } + + public String resolverAddr(byte[] node, String address) throws Exception + { + //use caching + String nodeData = Numeric.toHexString(node); + CachedENSRead resolverData = cachedNameReads.get(cacheKey(nodeData, address)); + String resolverAddr = resolverData != null ? resolverData.cachedResult : null; + + if (resolverData == null || !resolverData.isValid()) + { + final org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(FUNC_addr, + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(node)), + Arrays.>asList(new TypeReference
() + { + })); + + resolverAddr = getContractData(address, function, ""); + if (!TextUtils.isEmpty(resolverAddr) && resolverAddr.length() > 2) + { + cachedNameReads.put(cacheKey(nodeData, address), new CachedENSRead(resolverAddr)); + } + } + + return resolverAddr; + } + + public String encodeResolverAddr(byte[] node) + { + final org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(FUNC_addr, + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(node)), + Arrays.>asList(new TypeReference
() {})); + + return FunctionEncoder.encode(function); + } + + public EthCall resolve(byte[] name, byte[] data, String address) throws Exception + { + final org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(FUNC_RESOLVE, + Arrays.asList(new org.web3j.abi.datatypes.DynamicBytes(name), + new org.web3j.abi.datatypes.DynamicBytes(data)), + Arrays.>asList(new TypeReference() {})); + + String encodedFunction = FunctionEncoder.encode(function); + + org.web3j.protocol.core.methods.request.Transaction transaction + = createEthCallTransaction(TokenscriptFunction.ZERO_ADDRESS, address, encodedFunction); + return web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).send(); + } + + public EthCall resolveWithProof(byte[] response, byte[] extraData, String address) throws Exception + { + final org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(FUNC_RESOLVEWITHPROOF, + Arrays.asList(new org.web3j.abi.datatypes.DynamicBytes(response), + new org.web3j.abi.datatypes.DynamicBytes(extraData)), + Arrays.>asList(new TypeReference() {})); + + String encodedFunction = FunctionEncoder.encode(function); + + org.web3j.protocol.core.methods.request.Transaction transaction + = createEthCallTransaction(TokenscriptFunction.ZERO_ADDRESS, address, encodedFunction); + return web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).send(); + } + + private String resolveName(byte[] node, String address) throws Exception + { + final org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(FUNC_NAME, + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes32(node)), + Arrays.>asList(new TypeReference() {})); + + return getContractData(address, function, ""); + } + + public T getContractData(String address, Function function, T type) throws Exception + { + String responseValue = callSmartContractFunction(function, address); + + if (TextUtils.isEmpty(responseValue)) + { + throw new Exception("Bad contract value"); + } + else if (responseValue.equals("0x")) + { + if (type instanceof Boolean) + { + return (T) Boolean.FALSE; + } + else + { + return null; + } + } + + List response = FunctionReturnDecoder.decode( + responseValue, function.getOutputParameters()); + if (response.size() == 1) + { + return (T) response.get(0).getValue(); + } + else + { + if (type instanceof Boolean) + { + return (T) Boolean.FALSE; + } + else + { + return null; + } + } + } + + private String callSmartContractFunction( + Function function, String contractAddress) throws Exception + { + try + { + String encodedFunction = FunctionEncoder.encode(function); + + org.web3j.protocol.core.methods.request.Transaction transaction + = createEthCallTransaction(TokenscriptFunction.ZERO_ADDRESS, contractAddress, encodedFunction); + EthCall response = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).send(); + + return response.getValue(); + } + catch (InterruptedIOException | UnknownHostException | JsonParseException e) + { + //expected to happen when user switches wallets + return "0x"; + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/ens/Resolvable.java b/app/src/main/java/com/alphawallet/app/util/ens/Resolvable.java new file mode 100644 index 0000000000..eb4611517d --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ens/Resolvable.java @@ -0,0 +1,6 @@ +package com.alphawallet.app.util.ens; + +public interface Resolvable +{ + String resolve(String ensName) throws Exception; +} diff --git a/app/src/main/java/com/alphawallet/app/util/ens/UnstoppableDomainsResolver.java b/app/src/main/java/com/alphawallet/app/util/ens/UnstoppableDomainsResolver.java new file mode 100644 index 0000000000..ff138e57e5 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ens/UnstoppableDomainsResolver.java @@ -0,0 +1,90 @@ +package com.alphawallet.app.util.ens; + +import android.net.Uri; +import android.text.TextUtils; + +import com.alphawallet.app.entity.unstoppable.GetRecordsResult; +import com.alphawallet.app.repository.KeyProvider; +import com.alphawallet.app.repository.KeyProviderFactory; +import com.alphawallet.ethereum.EthereumNetworkBase; +import com.google.gson.Gson; + +import java.util.HashMap; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import timber.log.Timber; + +public class UnstoppableDomainsResolver implements Resolvable +{ + private static final String GET_RECORDS_FOR_DOMAIN = "https://resolve.unstoppabledomains.com/domains/"; + private final KeyProvider keyProvider = KeyProviderFactory.get(); + private final OkHttpClient client; + private final long chainId; + + public UnstoppableDomainsResolver(OkHttpClient client, long chainId) + { + this.client = client; + this.chainId = chainId; + } + + @Override + public String resolve(String domainName) throws Exception + { + Uri.Builder builder = new Uri.Builder(); + builder.encodedPath(GET_RECORDS_FOR_DOMAIN) + .appendEncodedPath(domainName); + + Request request = new Request.Builder() + .header("Authorization", "Bearer " + keyProvider.getUnstoppableDomainsKey()) + .url(builder.build().toString()) + .get() + .build(); + + try (okhttp3.Response response = client.newCall(request).execute()) + { + ResponseBody responseBody = response.body(); + if (responseBody != null) + { + GetRecordsResult result = new Gson().fromJson(responseBody.string(), GetRecordsResult.class); + response.close(); + return getAddressFromRecords(result.records, chainId); + } + response.close(); + return ""; + } + catch (Exception e) + { + Timber.e(e); + } + + return ""; + } + + private String getAddressFromRecords(HashMap records, long chainId) + { + String ethAddress = records.getOrDefault("crypto.ETH.address", ""); + String maticAddress = records.getOrDefault("crypto.MATIC.version.MATIC.address", ""); + if (chainId == EthereumNetworkBase.MAINNET_ID) + { + return ethAddress; + } + else if (chainId == EthereumNetworkBase.POLYGON_ID) + { + return maticAddress; + } + else + { + if (!TextUtils.isEmpty(ethAddress)) + { + return ethAddress; + } + else if (!TextUtils.isEmpty(maticAddress)) + { + return maticAddress; + } + } + return ""; + } +} diff --git a/app/src/main/java/com/alphawallet/app/util/pattern/Patterns.java b/app/src/main/java/com/alphawallet/app/util/pattern/Patterns.java new file mode 100644 index 0000000000..cf15e07199 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/pattern/Patterns.java @@ -0,0 +1,79 @@ +package com.alphawallet.app.util.pattern; + +import java.util.regex.Pattern; + +/** + * Created by JB on 1/11/2022. + * + * Lifted from Android OS standard 'Patterns' file + * Reason for separation :- Android file not available for test suite + * + */ +public class Patterns +{ + private static final String UCS_CHAR = "[" + + "\u00A0-\uD7FF" + + "\uF900-\uFDCF" + + "\uFDF0-\uFFEF" + + "\uD800\uDC00-\uD83F\uDFFD" + + "\uD840\uDC00-\uD87F\uDFFD" + + "\uD880\uDC00-\uD8BF\uDFFD" + + "\uD8C0\uDC00-\uD8FF\uDFFD" + + "\uD900\uDC00-\uD93F\uDFFD" + + "\uD940\uDC00-\uD97F\uDFFD" + + "\uD980\uDC00-\uD9BF\uDFFD" + + "\uD9C0\uDC00-\uD9FF\uDFFD" + + "\uDA00\uDC00-\uDA3F\uDFFD" + + "\uDA40\uDC00-\uDA7F\uDFFD" + + "\uDA80\uDC00-\uDABF\uDFFD" + + "\uDAC0\uDC00-\uDAFF\uDFFD" + + "\uDB00\uDC00-\uDB3F\uDFFD" + + "\uDB44\uDC00-\uDB7F\uDFFD" + + "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]"; + + private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR; + + /** + * Valid characters for IRI TLD defined in RFC 3987. + */ + + private static final String WORD_BOUNDARY = "(?:\\b|$|^)"; + private static final String IRI_LABEL = + "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"; + private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w"; + private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR; + private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")"; + + private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@"; + + private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD; + + private static final String IP_ADDRESS_STRING = + "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9]))"; + + private static final String DOMAIN_NAME_STR = "(" + HOST_NAME + "|" + IP_ADDRESS_STRING + ")"; + public static final Pattern DOMAIN_NAME = Pattern.compile(DOMAIN_NAME_STR); + + private static final String PROTOCOL = "(?i:http|https|rtsp|ftp)://"; + + private static final String PORT_NUMBER = "\\:\\d{1,5}"; + + private static final String PATH_AND_QUERY = "[/\\?](?:(?:[" + LABEL_CHAR + + ";/\\?:@&=#~" // plus optional query params + + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*"; + + public static Pattern WEB_URL = Pattern.compile("(" + + "(" + + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?" + + "(?:" + DOMAIN_NAME_STR + ")" + + "(?:" + PORT_NUMBER + ")?" + + ")" + + "(" + PATH_AND_QUERY + ")?" + + WORD_BOUNDARY + + ")"); +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java index 23da4def63..cb956a3a2d 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java @@ -9,6 +9,7 @@ import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.RealmManager; import com.alphawallet.app.service.TokensService; @@ -59,13 +60,15 @@ public LiveData defaultWallet() { AssetDefinitionService assetDefinitionService, TokensService tokensService, TransactionsService transactionsService, - RealmManager realmManager) { + RealmManager realmManager, + AnalyticsServiceType analyticsService) { this.genericWalletInteract = genericWalletInteract; this.fetchTransactionsInteract = fetchTransactionsInteract; this.assetDefinitionService = assetDefinitionService; this.tokensService = tokensService; this.transactionsService = transactionsService; this.realmManager = realmManager; + setAnalyticsService(analyticsService); } public void prepare() diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/AddEditDappViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/AddEditDappViewModel.java new file mode 100644 index 0000000000..886cac7418 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/AddEditDappViewModel.java @@ -0,0 +1,17 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.service.AnalyticsServiceType; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AddEditDappViewModel extends BaseViewModel +{ + @Inject + AddEditDappViewModel(AnalyticsServiceType analyticsService) + { + setAnalyticsService(analyticsService); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/AddTokenViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/AddTokenViewModel.java index 383a8bf583..aeca278714 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/AddTokenViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/AddTokenViewModel.java @@ -2,8 +2,10 @@ import android.content.Context; import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.text.format.DateUtils; -import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -14,11 +16,9 @@ import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenInfo; -import com.alphawallet.app.interact.FetchTokensInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; -import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.ui.ImportTokenActivity; @@ -26,6 +26,7 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import javax.annotation.Nullable; @@ -37,13 +38,13 @@ import io.reactivex.schedulers.Schedulers; @HiltViewModel -public class AddTokenViewModel extends BaseViewModel { +public class AddTokenViewModel extends BaseViewModel +{ private final MutableLiveData wallet = new MutableLiveData<>(); - private final MutableLiveData tokenInfo = new MutableLiveData<>(); private final MutableLiveData switchNetwork = new MutableLiveData<>(); private final MutableLiveData finalisedToken = new MutableLiveData<>(); - private final MutableLiveData tokentype = new MutableLiveData<>(); + private final MutableLiveData tokenType = new MutableLiveData<>(); private final MutableLiveData noContract = new MutableLiveData<>(); private final MutableLiveData scanCount = new MutableLiveData<>(); @@ -52,25 +53,45 @@ public class AddTokenViewModel extends BaseViewModel { private final EthereumNetworkRepositoryType ethereumNetworkRepository; private final GenericWalletInteract genericWalletInteract; - private final FetchTokensInteract fetchTokensInteract; private final FetchTransactionsInteract fetchTransactionsInteract; private final AssetDefinitionService assetDefinitionService; private final TokensService tokensService; - private final PreferenceRepositoryType sharedPreference; private boolean foundNetwork; private int networkCount; private long primaryChainId = 1; private final List discoveredTokenList = new ArrayList<>(); + private final Handler handler = new Handler(Looper.getMainLooper()); - public MutableLiveData wallet() { + public MutableLiveData wallet() + { return wallet; } - public MutableLiveData tokenType() { return tokentype; } - public LiveData switchNetwork() { return switchNetwork; } - public LiveData chainScanCount() { return scanCount; } - public LiveData onToken() { return onToken; } - public LiveData allTokens() { return allTokens; } + + public MutableLiveData tokenType() + { + return tokenType; + } + + public LiveData switchNetwork() + { + return switchNetwork; + } + + public LiveData chainScanCount() + { + return scanCount; + } + + public LiveData onToken() + { + return onToken; + } + + public LiveData allTokens() + { + return allTokens; + } @Nullable Disposable scanNetworksDisposable; @@ -80,19 +101,16 @@ public MutableLiveData wallet() { @Inject AddTokenViewModel( GenericWalletInteract genericWalletInteract, - FetchTokensInteract fetchTokensInteract, EthereumNetworkRepositoryType ethereumNetworkRepository, FetchTransactionsInteract fetchTransactionsInteract, AssetDefinitionService assetDefinitionService, - TokensService tokensService, - PreferenceRepositoryType sharedPreference) { + TokensService tokensService) + { this.genericWalletInteract = genericWalletInteract; - this.fetchTokensInteract = fetchTokensInteract; this.ethereumNetworkRepository = ethereumNetworkRepository; this.fetchTransactionsInteract = fetchTransactionsInteract; this.assetDefinitionService = assetDefinitionService; this.tokensService = tokensService; - this.sharedPreference = sharedPreference; } public void saveTokens(List toSave) @@ -131,7 +149,7 @@ private void checkType(Throwable throwable, long chainId, String address, Contra public void fetchToken(long chainId, String addr) { - tokensService.update(addr, chainId) + tokensService.update(addr, chainId, ContractType.NOT_SET) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::gotTokenUpdate, this::onError).isDisposed(); @@ -150,7 +168,10 @@ private void resumeSend(Token token) finalisedToken.postValue(token); } - public NetworkInfo getNetworkInfo(long chainId) { return ethereumNetworkRepository.getNetworkByChain(chainId); } + public NetworkInfo getNetworkInfo(long chainId) + { + return ethereumNetworkRepository.getNetworkByChain(chainId); + } private void findWallet() { @@ -158,7 +179,8 @@ private void findWallet() .subscribe(wallet::setValue, this::onError); } - private void onTokensSetup(TokenInfo info) { + private void onTokensSetup(TokenInfo info) + { disposable = tokensService.addToken(info, wallet.getValue().address) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -176,7 +198,7 @@ private void tokenTypeError(Throwable throwable, TokenInfo data) { checkNetworkCount(); Token badToken = new Token(data, BigDecimal.ZERO, 0, "", ContractType.NOT_SET); - tokentype.postValue(badToken); + tokenType.postValue(badToken); } public void prepare() @@ -222,6 +244,7 @@ private List getNetworkIds() { if (!networkIds.contains(networkInfo.chainId)) networkIds.add(networkInfo.chainId); } + return networkIds; } @@ -245,6 +268,8 @@ public void testNetworks(String address) scanThreads.add(d); } + + handler.postDelayed(this::stopScan, 60 * DateUtils.SECOND_IN_MILLIS); } private void testNetworkResult(final TokenInfo info, final ContractType type) @@ -253,7 +278,7 @@ private void testNetworkResult(final TokenInfo info, final ContractType type) { foundNetwork = true; disposable = tokensService - .update(info.address, info.chainId) + .update(info.address, info.chainId, type) .subscribe(this::onTokensSetup, error -> checkType(error, info.chainId, info.address, type)); } else @@ -269,6 +294,8 @@ public void stopScan() if (!d.isDisposed()) d.dispose(); } scanThreads.clear(); + scanCount.postValue(0); + handler.removeCallbacksAndMessages(null); } private void onTestError(Throwable throwable) @@ -313,4 +340,25 @@ public AssetDefinitionService getAssetDefinitionService() { return assetDefinitionService; } + + public EthereumNetworkRepositoryType ethereumNetworkRepository() + { + return ethereumNetworkRepository; + } + + public void setMainNetsSelected(boolean mainNetSelected) + { + ethereumNetworkRepository.setActiveMainnet(mainNetSelected); + } + + public void selectExtraChains(List selectedChains) + { + //add new chains to chain selection + //get current list and add it on + HashSet uniqueList = new HashSet<>(selectedChains); + uniqueList.addAll(ethereumNetworkRepository.getFilterNetworkList()); + ethereumNetworkRepository.setFilterNetworkList(uniqueList.toArray(new Long[0])); + ethereumNetworkRepository.commitPrefs(); + tokensService.setupFilter(true); + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/AnalyticsSettingsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/AnalyticsSettingsViewModel.java new file mode 100644 index 0000000000..82f6850635 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/AnalyticsSettingsViewModel.java @@ -0,0 +1,40 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.repository.PreferenceRepositoryType; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AnalyticsSettingsViewModel extends BaseViewModel +{ + private final PreferenceRepositoryType preferenceRepository; + + @Inject + AnalyticsSettingsViewModel(PreferenceRepositoryType preferenceRepository) + { + this.preferenceRepository = preferenceRepository; +// setAnalyticsService(analyticsService); + } + + public boolean isAnalyticsEnabled() + { + return preferenceRepository.isAnalyticsEnabled(); + } + + public boolean isCrashReportingEnabled() + { + return preferenceRepository.isCrashReportingEnabled(); + } + + public void toggleAnalytics(boolean isEnabled) + { + preferenceRepository.setAnalyticsEnabled(isEnabled); + } + + public void toggleCrashReporting(boolean isEnabled) + { + preferenceRepository.setCrashReportingEnabled(isEnabled); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/ApiV1ViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/ApiV1ViewModel.java index cfe4459856..3cb61df298 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/ApiV1ViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/ApiV1ViewModel.java @@ -1,8 +1,6 @@ package com.alphawallet.app.viewmodel; -import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; - import android.app.Activity; import android.net.Uri; import android.text.TextUtils; @@ -76,7 +74,7 @@ private void onDefaultWallet(final Wallet wallet) public void signMessage(Signable message) { - disposable = createTransactionInteract.sign(defaultWallet.getValue(), message, MAINNET_ID) + disposable = createTransactionInteract.sign(defaultWallet.getValue(), message) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onSignSuccess, this::onSignError); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/BackupKeyViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/BackupKeyViewModel.java index f88f656007..b5aa7569ed 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/BackupKeyViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/BackupKeyViewModel.java @@ -1,11 +1,12 @@ package com.alphawallet.app.viewmodel; import android.app.Activity; +import android.text.TextUtils; +import android.util.Pair; + import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import android.text.TextUtils; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.entity.CreateWalletCallbackInterface; import com.alphawallet.app.entity.ErrorEnvelope; @@ -171,5 +172,20 @@ public void failedAuthentication(Operation taskCode) { keyService.failedAuthentication(taskCode); } + + public boolean hasKey(String address) + { + return keyService.hasKeystore(address); + } + + public Single storeWallet(Wallet wallet) + { + return fetchWalletsInteract.storeWallet(wallet); + } + + public Pair testCipher(String walletAddress, String cipherAlgorithm) + { + return keyService.testCipher(walletAddress, cipherAlgorithm); + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/BaseViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/BaseViewModel.java index fd35da3f75..2f30e1de81 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/BaseViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/BaseViewModel.java @@ -1,109 +1,170 @@ package com.alphawallet.app.viewmodel; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; - import android.app.Activity; import android.content.Context; import android.text.TextUtils; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + import com.alphawallet.app.C; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.ServiceException; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.service.AnalyticsServiceType; + +import org.json.JSONObject; + import io.reactivex.disposables.Disposable; import timber.log.Timber; public class BaseViewModel extends ViewModel { - protected final MutableLiveData error = new MutableLiveData<>(); - protected final MutableLiveData progress = new MutableLiveData<>(); - protected Disposable disposable; - protected static final MutableLiveData queueCompletion = new MutableLiveData<>(); - protected static final MutableLiveData pushToastMutable = new MutableLiveData<>(); - protected static final MutableLiveData successDialogMutable = new MutableLiveData<>(); - protected static final MutableLiveData errorDialogMutable = new MutableLiveData<>(); - protected static final MutableLiveData refreshTokens = new MutableLiveData<>(); - - @Override - protected void onCleared() - { - cancel(); - } - - private void cancel() - { - if (disposable != null && !disposable.isDisposed()) - { - disposable.dispose(); - } - } - - public LiveData error() - { - return error; - } - - public LiveData progress() - { - return progress; - } - - public LiveData queueProgress() - { - return queueCompletion; - } - - public LiveData pushToast() - { - return pushToastMutable; - } - - public LiveData refreshTokens() { - return refreshTokens; - } - - protected void onError(Throwable throwable) - { - Timber.tag("TAG").d(throwable, "Err"); - if (throwable instanceof ServiceException) - { - error.postValue(((ServiceException) throwable).error); - } - else - { - String message = throwable.getMessage(); - if (TextUtils.isEmpty(message)) - { - error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, null, throwable)); - } - else - { - error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, message, throwable)); - } - } - } - - public static void onQueueUpdate(int complete) - { - queueCompletion.postValue(complete); - } - - public static void onPushToast(String message) - { - pushToastMutable.postValue(message); - } - - public void showSendToken(Context context, String address, String symbol, int decimals, Token token) { - //do nothing - } - - public void showTokenList(Activity activity, Token token) { - //do nothing - } - - public void showErc20TokenDetail(Activity context, String address, String symbol, int decimals, Token token) { - //do nothing - } + protected static final MutableLiveData queueCompletion = new MutableLiveData<>(); + protected static final MutableLiveData pushToastMutable = new MutableLiveData<>(); + protected static final MutableLiveData successDialogMutable = new MutableLiveData<>(); + protected static final MutableLiveData errorDialogMutable = new MutableLiveData<>(); + protected static final MutableLiveData refreshTokens = new MutableLiveData<>(); + protected final MutableLiveData error = new MutableLiveData<>(); + protected final MutableLiveData progress = new MutableLiveData<>(); + protected Disposable disposable; + private AnalyticsServiceType analyticsService; + + public static void onQueueUpdate(int complete) + { + queueCompletion.postValue(complete); + } + + public static void onPushToast(String message) + { + pushToastMutable.postValue(message); + } + + @Override + protected void onCleared() + { + cancel(); + } + + private void cancel() + { + if (disposable != null && !disposable.isDisposed()) + { + disposable.dispose(); + } + } + + public LiveData error() + { + return error; + } + + public LiveData progress() + { + return progress; + } + + public LiveData queueProgress() + { + return queueCompletion; + } + + public LiveData pushToast() + { + return pushToastMutable; + } + + public LiveData refreshTokens() + { + return refreshTokens; + } + + protected void onError(Throwable throwable) + { + Timber.e(throwable); + if (throwable instanceof ServiceException) + { + error.postValue(((ServiceException) throwable).error); + } + else + { + String message = TextUtils.isEmpty(throwable.getMessage()) ? + "Unknown Error" : throwable.getMessage(); + error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, message, throwable)); + } + } + + public void showSendToken(Context context, String address, String symbol, int decimals, Token token) + { + //do nothing + } + + public void showTokenList(Activity activity, Token token) + { + //do nothing + } + + public void showErc20TokenDetail(Activity context, String address, String symbol, int decimals, Token token) + { + //do nothing + } + + protected void setAnalyticsService(AnalyticsServiceType analyticsService) + { + this.analyticsService = analyticsService; + } + + public void identify(String uuid) + { + if (analyticsService != null) + { + analyticsService.identify(uuid); + } + } + + public void track(Analytics.Navigation event) + { + trackEvent(event.getValue()); + } + + public void track(Analytics.Navigation event, AnalyticsProperties props) + { + trackEventWithProps(event.getValue(), props); + } + + public void track(Analytics.Action event) + { + trackEvent(event.getValue()); + } + + public void track(Analytics.Action event, AnalyticsProperties props) + { + trackEventWithProps(event.getValue(), props); + } + + public void trackError(Analytics.Error source, String message) + { + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ERROR_MESSAGE, message); + trackEventWithProps(source.getValue(), props); + } + + private void trackEvent(String event) + { + if (analyticsService != null) + { + analyticsService.track(event); + } + } + + private void trackEventWithProps(String event, AnalyticsProperties props) + { + if (analyticsService != null) + { + analyticsService.track(event, props); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/BrowserHistoryViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/BrowserHistoryViewModel.java new file mode 100644 index 0000000000..ea2500058d --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/BrowserHistoryViewModel.java @@ -0,0 +1,17 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.service.AnalyticsServiceType; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class BrowserHistoryViewModel extends BaseViewModel +{ + @Inject + BrowserHistoryViewModel(AnalyticsServiceType analyticsService) + { + setAnalyticsService(analyticsService); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/CoinbasePayViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/CoinbasePayViewModel.java new file mode 100644 index 0000000000..8f0584d347 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/CoinbasePayViewModel.java @@ -0,0 +1,62 @@ +package com.alphawallet.app.viewmodel; + + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.coinbasepay.DestinationWallet; +import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.repository.CoinbasePayRepositoryType; +import com.alphawallet.app.service.AnalyticsServiceType; + +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.disposables.Disposable; + +@HiltViewModel +public class CoinbasePayViewModel extends BaseViewModel +{ + private final GenericWalletInteract genericWalletInteract; + private final CoinbasePayRepositoryType coinbasePayRepository; + private final MutableLiveData defaultWallet = new MutableLiveData<>(); + private final MutableLiveData signature = new MutableLiveData<>(); + + protected Disposable disposable; + + @Inject + public CoinbasePayViewModel(GenericWalletInteract genericWalletInteract, + CoinbasePayRepositoryType coinbasePayRepository, + AnalyticsServiceType analyticsService) + { + this.genericWalletInteract = genericWalletInteract; + this.coinbasePayRepository = coinbasePayRepository; + setAnalyticsService(analyticsService); + } + + public LiveData defaultWallet() + { + return defaultWallet; + } + + public void prepare() + { + progress.postValue(false); + disposable = genericWalletInteract + .find() + .subscribe(this::onDefaultWallet, this::onError); + } + + private void onDefaultWallet(final Wallet wallet) + { + defaultWallet.setValue(wallet); + } + + public String getUri(DestinationWallet.Type type, String address, List list) + { + return coinbasePayRepository.getUri(type, address, list); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java index 1f1182a660..eec78d7c4e 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java @@ -1,8 +1,11 @@ package com.alphawallet.app.viewmodel; +import com.alphawallet.app.analytics.Analytics; +import com.alphawallet.app.entity.AnalyticsProperties; +import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; -import com.alphawallet.app.entity.NetworkInfo; +import com.alphawallet.app.service.AnalyticsServiceType; import javax.inject.Inject; @@ -15,20 +18,32 @@ public class CustomNetworkViewModel extends BaseViewModel @Inject CustomNetworkViewModel( - EthereumNetworkRepositoryType ethereumNetworkRepository) + EthereumNetworkRepositoryType ethereumNetworkRepository, + AnalyticsServiceType analyticsService) { this.ethereumNetworkRepository = ethereumNetworkRepository; + setAnalyticsService(analyticsService); } - public void saveNetwork(String name, String rpcUrl, long chainId, String symbol, String blockExplorerUrl, String explorerApiUrl, boolean isTestnet, Long oldChainId) { + public void saveNetwork(boolean isEditMode, String name, String rpcUrl, long chainId, String symbol, String blockExplorerUrl, String explorerApiUrl, boolean isTestnet, Long oldChainId) + { this.ethereumNetworkRepository.saveCustomRPCNetwork(name, rpcUrl, chainId, symbol, blockExplorerUrl, explorerApiUrl, isTestnet, oldChainId); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_CUSTOM_NETWORK_NAME, name); + props.put(Analytics.PROPS_CUSTOM_NETWORK_RPC_URL, rpcUrl); + props.put(Analytics.PROPS_CUSTOM_NETWORK_CHAIN_ID, chainId); + props.put(Analytics.PROPS_CUSTOM_NETWORK_SYMBOL, symbol); + props.put(Analytics.PROPS_CUSTOM_NETWORK_IS_TESTNET, isTestnet); + track(isEditMode? Analytics.Action.EDIT_CUSTOM_CHAIN : Analytics.Action.ADD_CUSTOM_CHAIN, props); } - public NetworkInfo getNetworkInfo(long chainId) { + public NetworkInfo getNetworkInfo(long chainId) + { return this.ethereumNetworkRepository.getNetworkByChain(chainId); } - public boolean isTestNetwork(NetworkInfo network) { + public boolean isTestNetwork(NetworkInfo network) + { return !EthereumNetworkRepository.hasRealValue(network.chainId); } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/DappBrowserViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/DappBrowserViewModel.java index 84978da9ff..0608384765 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/DappBrowserViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/DappBrowserViewModel.java @@ -1,6 +1,7 @@ package com.alphawallet.app.viewmodel; import static com.alphawallet.app.C.Key.WALLET; +import static com.alphawallet.app.util.Utils.isValidUrl; import android.app.Activity; import android.content.Context; @@ -9,6 +10,7 @@ import android.webkit.WebView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -16,17 +18,19 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.DApp; -import com.alphawallet.app.entity.DAppFunction; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.QRResult; import com.alphawallet.app.entity.SendTransactionInterface; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.analytics.QrScanSource; import com.alphawallet.app.interact.CreateTransactionInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.GasService; import com.alphawallet.app.service.KeyService; @@ -35,15 +39,17 @@ import com.alphawallet.app.ui.HomeActivity; import com.alphawallet.app.ui.ImportTokenActivity; import com.alphawallet.app.ui.MyAddressActivity; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.SendActivity; import com.alphawallet.app.ui.WalletConnectActivity; +import com.alphawallet.app.ui.WalletConnectV2Activity; import com.alphawallet.app.util.DappBrowserUtils; +import com.alphawallet.app.walletconnect.util.WalletConnectHelper; import com.alphawallet.app.web3.entity.WalletAddEthereumChainObject; import com.alphawallet.app.web3.entity.Web3Transaction; -import com.alphawallet.token.entity.Signable; -import java.math.BigDecimal; +import org.web3j.utils.Numeric; + import java.math.BigInteger; import java.util.List; import java.util.concurrent.TimeUnit; @@ -53,13 +59,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel; import io.reactivex.Observable; import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; @HiltViewModel -public class DappBrowserViewModel extends BaseViewModel { +public class DappBrowserViewModel extends BaseViewModel +{ private static final int BALANCE_CHECK_INTERVAL_SECONDS = 20; private final MutableLiveData activeNetwork = new MutableLiveData<>(); @@ -83,7 +89,9 @@ public class DappBrowserViewModel extends BaseViewModel { TokensService tokensService, EthereumNetworkRepositoryType ethereumNetworkRepository, KeyService keyService, - GasService gasService) { + GasService gasService, + AnalyticsServiceType analyticsService) + { this.genericWalletInteract = genericWalletInteract; this.assetDefinitionService = assetDefinitionService; this.createTransactionInteract = createTransactionInteract; @@ -91,21 +99,26 @@ public class DappBrowserViewModel extends BaseViewModel { this.ethereumNetworkRepository = ethereumNetworkRepository; this.keyService = keyService; this.gasService = gasService; + setAnalyticsService(analyticsService); } - public AssetDefinitionService getAssetDefinitionService() { + public AssetDefinitionService getAssetDefinitionService() + { return assetDefinitionService; } - public LiveData activeNetwork() { + public LiveData activeNetwork() + { return activeNetwork; } - public LiveData defaultWallet() { + public LiveData defaultWallet() + { return defaultWallet; } - public void findWallet() { + public void findWallet() + { disposable = genericWalletInteract .find() .subscribe(this::onDefaultWallet, this::onError); @@ -121,7 +134,8 @@ public void checkForNetworkChanges() activeNetwork.postValue(ethereumNetworkRepository.getActiveBrowserNetwork()); } - private void onDefaultWallet(final Wallet wallet) { + private void onDefaultWallet(final Wallet wallet) + { defaultWallet.setValue(wallet); } @@ -133,34 +147,29 @@ private void checkBalance(final Wallet wallet) disposable = tokensService.getChainBalance(wallet.address.toLowerCase(), info.chainId) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) - .subscribe(w -> { }, e -> { }); + .subscribe(w -> {}, e -> {}); } } - public void signMessage(Signable message, DAppFunction dAppFunction) { - disposable = createTransactionInteract.sign(defaultWallet.getValue(), message, - getActiveNetwork().chainId) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(sig -> dAppFunction.DAppReturn(sig.signature, message), - error -> dAppFunction.DAppError(error, message)); - } - - public void setLastUrl(Context context, String url) { + public void setLastUrl(Context context, String url) + { PreferenceManager.getDefaultSharedPreferences(context) .edit().putString(C.DAPP_LASTURL_KEY, url).apply(); } - public void setHomePage(Context context, String url) { + public void setHomePage(Context context, String url) + { PreferenceManager.getDefaultSharedPreferences(context) .edit().putString(C.DAPP_HOMEPAGE_KEY, url).apply(); } - public String getHomePage(Context context) { + public String getHomePage(Context context) + { return PreferenceManager.getDefaultSharedPreferences(context).getString(C.DAPP_HOMEPAGE_KEY, null); } - public void addToMyDapps(Context context, String title, String url) { + public void addToMyDapps(Context context, String title, String url) + { Intent intent = new Intent(context, AddEditDappActivity.class); DApp dapp = new DApp(title, url); intent.putExtra(AddEditDappActivity.KEY_DAPP, dapp); @@ -168,7 +177,9 @@ public void addToMyDapps(Context context, String title, String url) { context.startActivity(intent); } - public void share(Context context, String url) { + public void share(Context context, String url) + { + track(Analytics.Action.SHARE_URL); Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, url); @@ -176,19 +187,24 @@ public void share(Context context, String url) { context.startActivity(intent); } - public void onClearBrowserCacheClicked(Context context) { + public void onClearBrowserCacheClicked(Context context) + { WebView webView = new WebView(context); webView.clearCache(true); Toast.makeText(context, context.getString(R.string.toast_browser_cache_cleared), Toast.LENGTH_SHORT).show(); + track(Analytics.Action.CLEAR_BROWSER_CACHE); } - public void startScan(Activity activity) { - Intent intent = new Intent(activity, QRScanner.class); + public void startScan(Activity activity) + { + Intent intent = new Intent(activity, QRScannerActivity.class); + intent.putExtra(QrScanSource.KEY, QrScanSource.BROWSER_SCREEN.getValue()); activity.startActivityForResult(intent, HomeActivity.DAPP_BARCODE_READER_REQUEST_CODE); } - public List getDappsMasterList(Context context) { + public List getDappsMasterList(Context context) + { return DappBrowserUtils.getDappsList(context); } @@ -254,14 +270,14 @@ public void sendTransaction(final Web3Transaction finalTx, long chainId, SendTra { disposable = createTransactionInteract .createWithSig(defaultWallet.getValue(), finalTx.gasPrice, finalTx.gasLimit, finalTx.payload, chainId) - .subscribe(txData -> callback.transactionSuccess(finalTx, txData.txHash), + .subscribe(txData -> callback.transactionSuccess(finalTx, txData.signature), error -> callback.transactionError(finalTx.leafPosition, error)); } else { disposable = createTransactionInteract .createWithSig(defaultWallet.getValue(), finalTx, chainId) - .subscribe(txData -> callback.transactionSuccess(finalTx, txData.txHash), + .subscribe(txData -> callback.transactionSuccess(finalTx, txData.signature), error -> callback.transactionError(finalTx.leafPosition, error)); } } @@ -275,7 +291,8 @@ public void showMyAddress(Context ctx) public void onDestroy() { - if (balanceTimerDisposable != null && !balanceTimerDisposable.isDisposed()) balanceTimerDisposable.dispose(); + if (balanceTimerDisposable != null && !balanceTimerDisposable.isDisposed()) + balanceTimerDisposable.dispose(); gasService.stopGasPriceCycle(); } @@ -300,19 +317,44 @@ public void startBalanceUpdate() public void stopBalanceUpdate() { - if (balanceTimerDisposable != null && !balanceTimerDisposable.isDisposed()) balanceTimerDisposable.dispose(); + if (balanceTimerDisposable != null && !balanceTimerDisposable.isDisposed()) + balanceTimerDisposable.dispose(); balanceTimerDisposable = null; gasService.stopGasPriceCycle(); } public void handleWalletConnect(Context context, String url, NetworkInfo activeNetwork) + { + Intent intent; + if (WalletConnectHelper.isWalletConnectV1(url)) + { + intent = getIntentOfWalletConnectV1(context, url, activeNetwork); + } + else + { + intent = getIntentOfWalletConnectV2(context, url); + } + + context.startActivity(intent); + } + + @NonNull + private Intent getIntentOfWalletConnectV2(Context context, String url) + { + Intent intent = new Intent(context, WalletConnectV2Activity.class); + intent.putExtra("url", url); + return intent; + } + + @NonNull + private Intent getIntentOfWalletConnectV1(Context context, String url, NetworkInfo activeNetwork) { String importPassData = WalletConnectActivity.WC_LOCAL_PREFIX + url; Intent intent = new Intent(context, WalletConnectActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); intent.putExtra(C.EXTRA_CHAIN_ID, activeNetwork.chainId); intent.putExtra("qrCode", importPassData); - context.startActivity(intent); + return intent; } public TokensService getTokenService() @@ -320,14 +362,23 @@ public TokensService getTokenService() return tokensService; } - public Single calculateGasEstimate(Wallet wallet, byte[] transactionBytes, long chainId, String sendAddress, BigDecimal sendAmount, BigInteger defaultGasLimit) + public Single calculateGasEstimate(Wallet wallet, Web3Transaction transaction, long chainId) { - return gasService.calculateGasEstimate(transactionBytes, chainId, sendAddress, sendAmount.toBigInteger(), wallet, defaultGasLimit); + if (transaction.isBaseTransfer()) + { + return Single.fromCallable(() -> BigInteger.valueOf(C.GAS_LIMIT_MIN)); + } + else + { + return gasService.calculateGasEstimate(Numeric.hexStringToByteArray(transaction.payload), chainId, + transaction.recipient.toString(), transaction.value, wallet, transaction.gasLimit); + } } + // Use the backup node if avail public String getNetworkNodeRPC(long chainId) { - return ethereumNetworkRepository.getNetworkByChain(chainId).rpcServerUrl; + return ethereumNetworkRepository.getDappBrowserRPC(chainId); } public NetworkInfo getNetworkInfo(long chainId) @@ -341,15 +392,49 @@ public String getSessionId(String url) return Uri.parse(uriString).getUserInfo(); } - public void addCustomChain(WalletAddEthereumChainObject chainObject) { - this.ethereumNetworkRepository.saveCustomRPCNetwork(chainObject.chainName, extractRpc(chainObject), chainObject.getChainId(), - chainObject.nativeCurrency.symbol, "", "", false, -1L); + public boolean addCustomChain(WalletAddEthereumChainObject chainObject) + { + String rpc = extractRpc(chainObject); + if (rpc == null) return false; + + this.ethereumNetworkRepository.saveCustomRPCNetwork(chainObject.chainName, rpc, chainObject.getChainId(), + chainObject.nativeCurrency.symbol, extractBlockExplorer(chainObject), "", false, -1L); tokensService.createBaseToken(chainObject.getChainId()) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) - .subscribe(w -> { }, e -> { }) + .subscribe(w -> {}, e -> {}) .isDisposed(); + + return true; + } + + private String extractBlockExplorer(WalletAddEthereumChainObject chainObject) + { + for (String thisRpc : chainObject.blockExplorerUrls) + { + if (isValidUrl(thisRpc)) //ensure RPC doesn't contain malicious code + { + String retRpc = thisRpc; + if (thisRpc.endsWith("/tx")) + { + retRpc = thisRpc + "/"; + } + else if (!thisRpc.endsWith("/tx/")) + { + if (!thisRpc.endsWith("/")) + { + retRpc = thisRpc + "/"; + } + + retRpc = retRpc + "tx/"; + } + + return retRpc; + } + } + + return ""; } //NB Chain descriptions can contain WSS socket defs, which might come first. @@ -357,10 +442,13 @@ private String extractRpc(WalletAddEthereumChainObject chainObject) { for (String thisRpc : chainObject.rpcUrls) { - if (thisRpc.toLowerCase().startsWith("http")) { return thisRpc; } + if (isValidUrl(thisRpc)) //ensure RPC doesn't contain malicious code + { + return thisRpc; + } } - return ""; + return null; } public boolean isMainNetsSelected() @@ -368,6 +456,11 @@ public boolean isMainNetsSelected() return ethereumNetworkRepository.isMainNetSelected(); } + public void setMainNetsSelected(boolean isMainNet) + { + ethereumNetworkRepository.setActiveMainnet(isMainNet); + } + public void addNetworkToFilters(NetworkInfo info) { List filters = ethereumNetworkRepository.getFilterNetworkList(); @@ -379,9 +472,4 @@ public void addNetworkToFilters(NetworkInfo info) tokensService.setupFilter(true); } - - public void setMainNetsSelected(boolean isMainNet) - { - ethereumNetworkRepository.setActiveMainnet(isMainNet); - } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java index 23c03216bf..1a7fe54bd7 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java @@ -17,6 +17,7 @@ import com.alphawallet.app.repository.OnRampRepositoryType; import com.alphawallet.app.router.MyAddressRouter; import com.alphawallet.app.router.SendTokenRouter; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TokensService; import com.alphawallet.token.entity.SigReturnType; @@ -52,13 +53,15 @@ public Erc20DetailViewModel(MyAddressRouter myAddressRouter, FetchTransactionsInteract fetchTransactionsInteract, AssetDefinitionService assetDefinitionService, TokensService tokensService, - OnRampRepositoryType onRampRepository) + OnRampRepositoryType onRampRepository, + AnalyticsServiceType analyticsService) { this.myAddressRouter = myAddressRouter; this.fetchTransactionsInteract = fetchTransactionsInteract; this.assetDefinitionService = assetDefinitionService; this.tokensService = tokensService; this.onRampRepository = onRampRepository; + setAnalyticsService(analyticsService); } public LiveData sig() diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index e73da539cb..87b51603ff 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -4,19 +4,14 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; import android.app.Activity; -import android.app.ActivityManager; -import android.app.DownloadManager; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.IBinder; @@ -25,22 +20,22 @@ import android.view.View; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; -import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.GitHubRelease; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.QRResult; import com.alphawallet.app.entity.Transaction; +import com.alphawallet.app.entity.Version; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletConnectActions; -import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; import com.alphawallet.app.interact.FetchWalletsInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.CurrencyRepositoryType; @@ -57,7 +52,6 @@ import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.RealmManager; -import com.alphawallet.app.service.TickerService; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.service.TransactionsService; import com.alphawallet.app.service.WalletConnectService; @@ -65,13 +59,11 @@ import com.alphawallet.app.ui.HomeActivity; import com.alphawallet.app.ui.ImportWalletActivity; import com.alphawallet.app.ui.SendActivity; -import com.alphawallet.app.util.AWEnsResolver; import com.alphawallet.app.util.QRParser; import com.alphawallet.app.util.RateApp; import com.alphawallet.app.util.Utils; +import com.alphawallet.app.util.ens.AWEnsResolver; import com.alphawallet.app.walletconnect.WCClient; -import com.alphawallet.app.walletconnect.WCSession; -import com.alphawallet.app.walletconnect.entity.WCPeerMeta; import com.alphawallet.app.walletconnect.entity.WCUtils; import com.alphawallet.app.widget.EmailPromptView; import com.alphawallet.app.widget.QRCodeActionsView; @@ -88,13 +80,10 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.UUID; +import java.util.concurrent.Callable; import javax.inject.Inject; @@ -110,11 +99,9 @@ import timber.log.Timber; @HiltViewModel -public class HomeViewModel extends BaseViewModel { - private final String TAG = "HVM"; +public class HomeViewModel extends BaseViewModel +{ public static final String ALPHAWALLET_DIR = "AlphaWallet"; - public static final String ALPHAWALLET_FILE_URL = "https://1x.alphawallet.com/dl/latest.apk"; - private final MutableLiveData defaultNetwork = new MutableLiveData<>(); private final MutableLiveData transactions = new MutableLiveData<>(); private final MutableLiveData backUpMessage = new MutableLiveData<>(); @@ -128,39 +115,35 @@ public class HomeViewModel extends BaseViewModel { private final CurrencyRepositoryType currencyRepository; private final EthereumNetworkRepositoryType ethereumNetworkRepository; private final TransactionsService transactionsService; - private final TickerService tickerService; private final MyAddressRouter myAddressRouter; - private final AnalyticsServiceType analyticsService; private final ExternalBrowserRouter externalBrowserRouter; private final OkHttpClient httpClient; private final RealmManager realmManager; - - private CryptoFunctions cryptoFunctions; - private ParseMagicLink parser; - - private final MutableLiveData installIntent = new MutableLiveData<>(); private final MutableLiveData walletName = new MutableLiveData<>(); private final MutableLiveData defaultWallet = new MutableLiveData<>(); private final MutableLiveData splashActivity = new MutableLiveData<>(); + private final MutableLiveData updateAvailable = new MutableLiveData<>(); + private CryptoFunctions cryptoFunctions; + private ParseMagicLink parser; private BottomSheetDialog dialog; @Inject HomeViewModel( - PreferenceRepositoryType preferenceRepository, - LocaleRepositoryType localeRepository, - ImportTokenRouter importTokenRouter, - AssetDefinitionService assetDefinitionService, - GenericWalletInteract genericWalletInteract, - FetchWalletsInteract fetchWalletsInteract, - CurrencyRepositoryType currencyRepository, - EthereumNetworkRepositoryType ethereumNetworkRepository, - MyAddressRouter myAddressRouter, - TransactionsService transactionsService, - TickerService tickerService, - AnalyticsServiceType analyticsService, - ExternalBrowserRouter externalBrowserRouter, - OkHttpClient httpClient, - RealmManager realmManager) { + PreferenceRepositoryType preferenceRepository, + LocaleRepositoryType localeRepository, + ImportTokenRouter importTokenRouter, + AssetDefinitionService assetDefinitionService, + GenericWalletInteract genericWalletInteract, + FetchWalletsInteract fetchWalletsInteract, + CurrencyRepositoryType currencyRepository, + EthereumNetworkRepositoryType ethereumNetworkRepository, + MyAddressRouter myAddressRouter, + TransactionsService transactionsService, + AnalyticsServiceType analyticsService, + ExternalBrowserRouter externalBrowserRouter, + OkHttpClient httpClient, + RealmManager realmManager) + { this.preferenceRepository = preferenceRepository; this.importTokenRouter = importTokenRouter; this.localeRepository = localeRepository; @@ -171,45 +154,53 @@ public class HomeViewModel extends BaseViewModel { this.ethereumNetworkRepository = ethereumNetworkRepository; this.myAddressRouter = myAddressRouter; this.transactionsService = transactionsService; - this.tickerService = tickerService; - this.analyticsService = analyticsService; this.externalBrowserRouter = externalBrowserRouter; this.httpClient = httpClient; this.realmManager = realmManager; - - + setAnalyticsService(analyticsService); this.preferenceRepository.incrementLaunchCount(); } @Override - protected void onCleared() { + protected void onCleared() + { super.onCleared(); } - public LiveData transactions() { + public LiveData transactions() + { return transactions; } - public LiveData installIntent() { - return installIntent; - } - - public LiveData backUpMessage() { + public LiveData backUpMessage() + { return backUpMessage; } - public LiveData splashReset() { + public LiveData splashReset() + { return splashActivity; } - public void prepare(Activity activity) { + public LiveData updateAvailable() + { + return updateAvailable; + } + + public LiveData defaultWallet() + { + return defaultWallet; + } + + public void prepare(Activity activity) + { progress.postValue(false); disposable = genericWalletInteract - .find() - .subscribe(w -> { - onDefaultWallet(w); - initWalletConnectSessions(activity, w); - }, this::onError); + .find() + .subscribe(w -> { + onDefaultWallet(w); + initWalletConnectSessions(activity, w); + }, this::onError); } public void onClean() @@ -222,40 +213,49 @@ private void onDefaultWallet(final Wallet wallet) defaultWallet.setValue(wallet); } - public void showImportLink(Activity activity, String importData) { + public void showImportLink(Activity activity, String importData) + { disposable = genericWalletInteract - .find().toObservable() - .filter(wallet -> checkWalletNotEqual(wallet, importData)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(wallet -> importLink(wallet, activity, importData), this::onError); + .find().toObservable() + .filter(wallet -> checkWalletNotEqual(wallet, importData)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(wallet -> importLink(activity, importData), this::onError); } - private boolean checkWalletNotEqual(Wallet wallet, String importData) { + private boolean checkWalletNotEqual(Wallet wallet, String importData) + { boolean filterPass = false; - try { - if (cryptoFunctions == null) { + try + { + if (cryptoFunctions == null) + { cryptoFunctions = new CryptoFunctions(); } - if (parser == null) { + if (parser == null) + { parser = new ParseMagicLink(cryptoFunctions, EthereumNetworkRepository.extraChains()); } MagicLinkData data = parser.parseUniversalLink(importData); String linkAddress = parser.getOwnerKey(data); - if (Utils.isAddressValid(data.contractAddress)) { + if (Utils.isAddressValid(data.contractAddress)) + { filterPass = !wallet.address.equals(linkAddress); } - } catch (Exception e) { - Timber.tag(TAG).e(e); + } + catch (Exception e) + { + Timber.e(e); } return filterPass; } - private void importLink(Wallet wallet, Activity activity, String importData) { + private void importLink(Activity activity, String importData) + { importTokenRouter.open(activity, importData); } @@ -267,56 +267,13 @@ public void restartHomeActivity(Context context) context.startActivity(intent); } - public void downloadAndInstall(String build, Context ctx) { - createDirectory(); - downloadAPK(build, ctx); - } - - private void createDirectory() { - //create XML repository directory - File directory = new File( - Environment.getExternalStorageDirectory() - + File.separator + ALPHAWALLET_DIR); - - if (!directory.exists()) { - directory.mkdir(); - } - } - - private void downloadAPK(String version, Context ctx) { - String destination = Environment.getExternalStorageDirectory() - + File.separator + ALPHAWALLET_DIR; - - File testFile = new File(destination, "AlphaWallet-" + version + ".apk"); - if (testFile.exists()) { - testFile.delete(); - } - final Uri uri = Uri.parse("file://" + testFile.getPath()); - - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(ALPHAWALLET_FILE_URL)); - request.setDescription(ctx.getString(R.string.alphawallet_update) + " " + version); - request.setTitle(ctx.getString(R.string.app_name)); - request.setDestinationUri(uri); - final DownloadManager manager = (DownloadManager) ctx.getSystemService(Context.DOWNLOAD_SERVICE); - long downloadId = manager.enqueue(request); - - //set BroadcastReceiver to install app when .apk is downloaded - BroadcastReceiver onComplete = new BroadcastReceiver() { - public void onReceive(Context ctxt, Intent intent) { - installIntent.postValue(testFile); - LocalBroadcastManager.getInstance(ctxt).unregisterReceiver(this); - } - }; - - LocalBroadcastManager.getInstance(ctx).registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); - } - - public void getWalletName(Context context) { + public void getWalletName(Context context) + { disposable = fetchWalletsInteract - .getWallet(preferenceRepository.getCurrentWalletAddress()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(wallet -> onWallet(context, wallet), this::walletError); + .getWallet(preferenceRepository.getCurrentWalletAddress()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(wallet -> updateWalletTitle(context, wallet), this::walletError); } private void walletError(Throwable throwable) @@ -325,7 +282,7 @@ private void walletError(Throwable throwable) splashActivity.postValue(true); } - private void onWallet(Context context, Wallet wallet) + private void updateWalletTitle(Context context, Wallet wallet) { transactionsService.changeWallet(wallet); if (!TextUtils.isEmpty(wallet.name)) @@ -341,48 +298,49 @@ else if (!TextUtils.isEmpty(wallet.ENSname)) walletName.postValue(""); //check for ENS name new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), context) - .reverseResolveEns(wallet.address) - .map(ensName -> { - wallet.ENSname = ensName; - return wallet; - }) - .flatMap(fetchWalletsInteract::updateENS) //store the ENS name - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe(updatedWallet -> walletName.postValue(updatedWallet.ENSname), this::onENSError).isDisposed(); + .reverseResolveEns(wallet.address) + .map(ensName -> { + wallet.ENSname = ensName; + return wallet; + }) + .flatMap(fetchWalletsInteract::updateENS) //store the ENS name + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(updatedWallet -> { + String name = Utils.formatAddress(wallet.address); + if (!TextUtils.isEmpty(updatedWallet.ENSname)) + { + name = updatedWallet.ENSname; + } + walletName.postValue(name); + }, this::onENSError).isDisposed(); } } - public LiveData walletName() { + public LiveData walletName() + { return walletName; } public void checkIsBackedUp(String walletAddress) { genericWalletInteract.getWalletNeedsBackup(walletAddress) - .subscribe(backUpMessage::postValue).isDisposed(); + .subscribe(backUpMessage::postValue).isDisposed(); } - public boolean isFindWalletAddressDialogShown() { + public boolean isFindWalletAddressDialogShown() + { return preferenceRepository.isFindWalletAddressDialogShown(); } - public void setFindWalletAddressDialogShown(boolean isShown) { - preferenceRepository.setFindWalletAddressDialogShown(isShown); - } - - public String getDefaultCurrency(){ - return currencyRepository.getDefaultCurrency(); - } - - public void updateTickers() + public void setFindWalletAddressDialogShown(boolean isShown) { - tickerService.updateTickers(); + preferenceRepository.setFindWalletAddressDialogShown(isShown); } private void onENSError(Throwable throwable) { - Timber.tag(TAG).e(throwable); + Timber.e(throwable); } public void setErrorCallback(FragmentMessenger callback) @@ -407,8 +365,6 @@ public void handleQRCode(Activity activity, String qrCode) showActionSheet(activity, qrResult); break; case PAYMENT: - showSend(activity, qrResult); - break; case TRANSFER: showSend(activity, qrResult); break; @@ -417,7 +373,7 @@ public void handleQRCode(Activity activity, String qrCode) //TODO: Code to generate the function signature will look like the code in generateTransactionFunction break; case URL: - ((HomeActivity)activity).onBrowserWithURL(qrCode); + ((HomeActivity) activity).onBrowserWithURL(qrCode); break; case MAGIC_LINK: showImportLink(activity, qrCode); @@ -432,13 +388,14 @@ public void handleQRCode(Activity activity, String qrCode) qrCode = null; } - if(qrCode == null) + if (qrCode == null) { Toast.makeText(activity, R.string.toast_invalid_code, Toast.LENGTH_SHORT).show(); } } - private void showActionSheet(Activity activity, QRResult qrResult) { + private void showActionSheet(Activity activity, QRResult qrResult) + { View.OnClickListener listener = v -> { if (v.getId() == R.id.send_to_this_address_action) @@ -465,7 +422,8 @@ else if (v.getId() == R.id.open_in_etherscan_action) Uri blockChainInfoUrl = info.getEtherscanAddressUri(qrResult.getAddress()); - if (blockChainInfoUrl != Uri.EMPTY) { + if (blockChainInfoUrl != Uri.EMPTY) + { externalBrowserRouter.open(activity, blockChainInfoUrl); } } @@ -493,7 +451,7 @@ else if (v.getId() == R.id.close_action) dialog.setContentView(contentView); dialog.setCancelable(true); dialog.setCanceledOnTouchOutside(true); - BottomSheetBehavior behavior = BottomSheetBehavior.from((View) contentView.getParent()); + BottomSheetBehavior behavior = BottomSheetBehavior.from((View) contentView.getParent()); dialog.setOnShowListener(dialog -> behavior.setPeekHeight(contentView.getHeight())); dialog.show(); } @@ -525,7 +483,7 @@ public void showMyAddress(Activity activity) * This method will uniquely identify the device by creating an ID and store in preference. * This will be changed if user reinstall application or clear the storage explicitly. **/ - public void identify(Context ctx) + public void identify() { String uuid = preferenceRepository.getUniqueId(); @@ -534,16 +492,9 @@ public void identify(Context ctx) uuid = UUID.randomUUID().toString(); } - analyticsService.identify(uuid); preferenceRepository.setUniqueId(uuid); - } - - public void actionSheetConfirm(String mode) - { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(mode); - analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); + identify(uuid); } public void checkTransactionEngine() @@ -566,28 +517,24 @@ public boolean fullScreenSelected() return preferenceRepository.getFullScreenState(); } - public void tryToShowRateAppDialog(Activity context) { + public void tryToShowRateAppDialog(Activity context) + { //only if installed from PlayStore (checked within the showRateTheApp method) RateApp.showRateTheApp(context, preferenceRepository, false); } - public int getUpdateWarnings() { + public int getUpdateWarnings() + { return preferenceRepository.getUpdateWarningCount(); } - public void setUpdateWarningCount(int warns) { + public void setUpdateWarningCount(int warns) + { preferenceRepository.setUpdateWarningCount(warns); } - public int getUpdateAsks() { - return preferenceRepository.getUpdateAsksCount(); - } - - public void setUpdateAsksCount(int asks) { - preferenceRepository.setUpdateAsksCount(asks); - } - - public void setInstallTime(int time) { + public void setInstallTime(int time) + { preferenceRepository.setInstallTime(time); } @@ -606,79 +553,75 @@ public int getLastFragmentId() return preferenceRepository.getLastFragmentPage(); } - public void tryToShowEmailPrompt(Context context, View successOverlay, Handler handler, Runnable onSuccessRunnable) { - if (preferenceRepository.getLaunchCount() == 4) { + public void tryToShowEmailPrompt(Context context, View successOverlay, Handler handler, Runnable onSuccessRunnable) + { + if (preferenceRepository.getLaunchCount() == 4) + { EmailPromptView emailPromptView = new EmailPromptView(context, successOverlay, handler, onSuccessRunnable); BottomSheetDialog emailPromptDialog = new BottomSheetDialog(context); emailPromptDialog.setContentView(emailPromptView); emailPromptDialog.setCancelable(true); emailPromptDialog.setCanceledOnTouchOutside(true); emailPromptView.setParentDialog(emailPromptDialog); - BottomSheetBehavior behavior = BottomSheetBehavior.from((View) emailPromptView.getParent()); + BottomSheetBehavior behavior = BottomSheetBehavior.from((View) emailPromptView.getParent()); emailPromptDialog.setOnShowListener(dialog -> behavior.setPeekHeight(emailPromptView.getHeight())); emailPromptDialog.show(); } } - public void tryToShowWhatsNewDialog(Context context) { + public void tryToShowWhatsNewDialog(Context context) + { PackageInfo packageInfo; - try { + try + { packageInfo = context.getPackageManager() - .getPackageInfo(context.getPackageName(), 0); + .getPackageInfo(context.getPackageName(), 0); int versionCode = packageInfo.versionCode; - if (preferenceRepository.getLastVersionCode(versionCode) < versionCode) { - // load what's new - Request request = new Request.Builder() - .header("Accept", "application/vnd.github.v3+json") - .url("https://api.github.com/repos/alphawallet/alpha-wallet-android/releases") - .get() - .build(); - - Single.fromCallable(() -> { - try (okhttp3.Response response = httpClient.newCall(request) - .execute()) { - return new Gson().>fromJson(response.body().string(), new TypeToken>() { - }.getType()); - } catch (Exception e) { - Timber.tag(TAG).e(e); - } - return null; - }).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()).subscribe((releases) -> { - - BottomSheetDialog dialog = new BottomSheetDialog(context); - - WhatsNewView view = new WhatsNewView(context, releases, v -> dialog.dismiss(), true); - dialog.setContentView(view); - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); - BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent()); - dialog.setOnShowListener(d -> behavior.setPeekHeight(view.getHeight())); - dialog.show(); - - preferenceRepository.setLastVersionCode(versionCode); - - }).isDisposed(); + if (preferenceRepository.getLastVersionCode(versionCode) < versionCode) + { + Request request = getRequest(); + Single.fromCallable(getGitHubReleases(request)).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()).subscribe((releases) -> { + doShowWhatsNewDialog(context, releases); + preferenceRepository.setLastVersionCode(versionCode); + }).isDisposed(); } - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + } + catch (PackageManager.NameNotFoundException e) + { + Timber.e(e); } } - private TokenDefinition parseFile(Context ctx, InputStream xmlInputStream) throws Exception { - Locale locale; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - locale = ctx.getResources().getConfiguration().getLocales().get(0); - } - else - { - locale = ctx.getResources().getConfiguration().locale; - } + private void doShowWhatsNewDialog(Context context, List releases) + { + BottomSheetDialog dialog = new BottomSheetDialog(context); + WhatsNewView view = new WhatsNewView(context, releases, v -> dialog.dismiss(), true); + dialog.setContentView(view); + dialog.setCancelable(true); + dialog.setCanceledOnTouchOutside(true); + BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent()); + dialog.setOnShowListener(d -> behavior.setPeekHeight(view.getHeight())); + dialog.show(); + } + + @NonNull + private Request getRequest() + { + return new Request.Builder() + .header("Accept", "application/vnd.github.v3+json") + .url("https://api.github.com/repos/alphawallet/alpha-wallet-android/releases") + .get() + .build(); + } + private TokenDefinition parseFile(Context ctx, InputStream xmlInputStream) throws Exception + { + Locale locale = ctx.getResources().getConfiguration().getLocales().get(0); return new TokenDefinition( - xmlInputStream, locale, null); + xmlInputStream, locale, null); } public void importScriptFile(Context ctx, boolean appExternal, Intent startIntent) @@ -727,7 +670,7 @@ public void importScriptFile(Context ctx, boolean appExternal, Intent startInten public boolean checkDebugDirectory() { File directory = new File(Environment.getExternalStorageDirectory() - + File.separator + ALPHAWALLET_DIR); + + File.separator + ALPHAWALLET_DIR); return directory.exists(); } @@ -746,7 +689,8 @@ public void setCurrencyAndLocale(Context context) currencyRepository.setDefaultCurrency(preferenceRepository.getDefaultCurrency()); } - public void sendMsgPumpToWC(Context context) { + public void sendMsgPumpToWC(Context context) + { Timber.d("Start WC service"); WCUtils.startServiceLocal(context, null, WalletConnectActions.MSG_PUMP); @@ -756,13 +700,13 @@ public void sendMsgPumpToWC(Context context) { private void initWalletConnectSessions(Activity activity, Wallet wallet) { List clientMap = new ArrayList<>(); - long cutOffTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS*2; + long cutOffTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS * 2; try (Realm realm = realmManager.getRealmInstance(WC_SESSION_DB)) { RealmResults items = realm.where(RealmWCSession.class) - .greaterThan("lastUsageTime", cutOffTime) - .sort("lastUsageTime", Sort.DESCENDING) - .findAll(); + .greaterThan("lastUsageTime", cutOffTime) + .sort("lastUsageTime", Sort.DESCENDING) + .findAll(); for (RealmWCSession r : items) { @@ -771,7 +715,7 @@ private void initWalletConnectSessions(Activity activity, Wallet wallet) { // restart the session if it's not already known by the service clientMap.add(WCUtils.createWalletConnectSession(activity, wallet, - r.getSession(), peerId, r.getRemotePeerId())); + r.getSession(), peerId, r.getRemotePeerId())); } } } @@ -796,10 +740,67 @@ public void onServiceConnected(ComponentName name, IBinder service) @Override public void onServiceDisconnected(ComponentName name) { - Timber.tag(TAG).d("Service disconnected"); + Timber.d("Service disconnected"); } }; WCUtils.startServiceLocal(activity, connection, WalletConnectActions.CONNECT); } + + public boolean checkNewWallet(String address) + { + return preferenceRepository.isNewWallet(address); + } + + public void setNewWallet(String address, boolean isNewWallet) + { + preferenceRepository.setNewWallet(address, isNewWallet); + } + + public void checkLatestGithubRelease() + { + Request request = getRequest(); + Single.fromCallable(getGitHubReleases(request)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((releases) -> { + GitHubRelease latestRelease = releases.get(0); + if (latestRelease != null) + { + String latestTag = latestRelease.getTagName(); + if (latestRelease.getTagName().charAt(0) == 'v') + { + latestTag = latestTag.substring(1); + } + Version latest = new Version(latestTag); + Version installed = new Version(BuildConfig.VERSION_NAME); + + if (latest.compareTo(installed) > 0) + { + updateAvailable.postValue(latest.get()); + } + } + }, Timber::e + ).isDisposed(); + } + + @NonNull + private Callable> getGitHubReleases(Request request) + { + return () -> + { + try (okhttp3.Response response = httpClient.newCall(request) + .execute()) + { + return new Gson().fromJson(response.body().string(), new TypeToken>() + { + }.getType()); + } + catch (Exception e) + { + Timber.e(e); + } + return null; + }; + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/ImportTokenViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/ImportTokenViewModel.java index d1f77e0345..480c065e0b 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/ImportTokenViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/ImportTokenViewModel.java @@ -45,8 +45,6 @@ import com.alphawallet.token.entity.XMLDsigDescriptor; import com.alphawallet.token.tools.ParseMagicLink; -import org.web3j.protocol.core.methods.response.EthEstimateGas; - import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -249,7 +247,7 @@ private void fetchToken() { private void setupTokenAddr(String contractAddress) { disposable = tokensService - .update(contractAddress, importOrder.chainId) + .update(contractAddress, importOrder.chainId, ContractType.NOT_SET) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(this::getTokenSpec, this::onError); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/ImportWalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/ImportWalletViewModel.java index 4c061c71d3..babd0d945f 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/ImportWalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/ImportWalletViewModel.java @@ -4,13 +4,14 @@ import android.app.Activity; +import androidx.core.util.Pair; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.alphawallet.app.C; -import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.ImportWalletCallback; +import com.alphawallet.app.entity.analytics.ImportWalletType; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.interact.ImportWalletInteract; @@ -18,7 +19,7 @@ import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.KeyService; import com.alphawallet.app.ui.widget.OnSetWatchWalletListener; -import com.alphawallet.app.util.AWEnsResolver; +import com.alphawallet.app.util.ens.AWEnsResolver; import com.fasterxml.jackson.databind.ObjectMapper; import org.web3j.crypto.ECKeyPair; @@ -26,6 +27,8 @@ import org.web3j.crypto.WalletFile; import org.web3j.utils.Numeric; +import java.math.BigInteger; + import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -40,24 +43,23 @@ public class ImportWalletViewModel extends BaseViewModel implements OnSetWatchWa private final ImportWalletInteract importWalletInteract; private final KeyService keyService; private final AWEnsResolver ensResolver; - private final AnalyticsServiceType analyticsService; - private final MutableLiveData wallet = new MutableLiveData<>(); + private final MutableLiveData> wallet = new MutableLiveData<>(); private final MutableLiveData badSeed = new MutableLiveData<>(); private final MutableLiveData watchExists = new MutableLiveData<>(); - private String importWalletType = ""; @Inject ImportWalletViewModel(ImportWalletInteract importWalletInteract, KeyService keyService, - AnalyticsServiceType analyticsService) { + AnalyticsServiceType analyticsService) + { this.importWalletInteract = importWalletInteract; this.keyService = keyService; this.ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), keyService.getContext()); - this.analyticsService = analyticsService; + setAnalyticsService(analyticsService); } - public void onKeystore(String keystore, String password, String newPassword, KeyService.AuthenticationLevel level) { - importWalletType = C.AN_KEYSTORE; + public void onKeystore(String keystore, String password, String newPassword, KeyService.AuthenticationLevel level) + { progress.postValue(true); importWalletInteract @@ -65,18 +67,37 @@ public void onKeystore(String keystore, String password, String newPassword, Key .flatMap(wallet -> importWalletInteract.storeKeystoreWallet(wallet, level, ensResolver)) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onWallet, this::onError).isDisposed(); + .subscribe(w -> onWallet(w, ImportWalletType.KEYSTORE), this::onError).isDisposed(); } - public void onPrivateKey(String privateKey, String newPassword, KeyService.AuthenticationLevel level) { - importWalletType = C.AN_PRIVATE_KEY; + public void onPrivateKey(String privateKey, String newPassword, KeyService.AuthenticationLevel level) + { progress.postValue(true); importWalletInteract .importPrivateKey(privateKey, newPassword) .flatMap(wallet -> importWalletInteract.storeKeystoreWallet(wallet, level, ensResolver)) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onWallet, this::onError).isDisposed(); + .subscribe(w -> onWallet(w, ImportWalletType.PRIVATE_KEY), this::onError).isDisposed(); + } + + public void onSeed(String walletAddress, KeyService.AuthenticationLevel level) + { + if (walletAddress == null) + { + Timber.e("walletAddress is null"); + progress.postValue(false); + badSeed.postValue(true); + } + else + { + progress.postValue(true); + //begin key storage process + disposable = importWalletInteract.storeHDWallet(walletAddress, level, ensResolver) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(w -> onWallet(w, ImportWalletType.SEED_PHRASE), this::onError); //signal to UI wallet import complete + } } @Override @@ -94,43 +115,33 @@ public void onWatchWallet(String address) disposable = importWalletInteract.storeWatchWallet(address, ensResolver) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(wallet::postValue, this::onError); //signal to UI wallet import complete + .subscribe(w -> onWallet(w, ImportWalletType.WATCH), this::onError); //signal to UI wallet import complete } - public LiveData wallet() { + public LiveData> wallet() + { return wallet; } - public LiveData badSeed() { return badSeed; } - public LiveData watchExists() { return watchExists; } - private void onWallet(Wallet wallet) { - progress.postValue(false); - this.wallet.postValue(wallet); - track(); + public LiveData badSeed() + { + return badSeed; } - public void onError(Throwable throwable) { - error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, throwable.getMessage())); + public LiveData watchExists() + { + return watchExists; } - public void onSeed(String walletAddress, KeyService.AuthenticationLevel level) + private void onWallet(Wallet wallet, ImportWalletType type) { - importWalletType = C.AN_SEED_PHRASE; - if (walletAddress == null) - { - progress.postValue(false); - Timber.e("ERROR"); - badSeed.postValue(true); - } - else - { - progress.postValue(true); - //begin key storage process - disposable = importWalletInteract.storeHDWallet(walletAddress, level, ensResolver) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onWallet, this::onError); //signal to UI wallet import complete - } + progress.postValue(false); + this.wallet.postValue(new Pair<>(wallet, type)); + } + + public void onError(Throwable throwable) + { + error.postValue(new ErrorEnvelope(C.ErrorCode.UNKNOWN, throwable.getMessage())); } // public void getAuthorisation(String walletAddress, Activity activity, SignAuthenticationCallback callback) @@ -167,6 +178,8 @@ public Single checkKeystorePassword(String keystore, String keystoreAdd ECKeyPair kp = org.web3j.crypto.Wallet.decrypt(password, walletFile); String address = Numeric.prependHexPrefix(Keys.getAddress(kp)); if (address.equalsIgnoreCase(keystoreAddress)) isValid = true; + //check public key + if (isValid && kp.getPublicKey().compareTo(BigInteger.ZERO) == 0) isValid = false; return isValid; }); } @@ -185,12 +198,4 @@ public void failedAuthentication(Operation taskCode) { keyService.failedAuthentication(taskCode); } - - public void track() - { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setWalletType(importWalletType); - - analyticsService.track(C.AN_IMPORT_WALLET, analyticsProperties); - } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/MyDappsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/MyDappsViewModel.java new file mode 100644 index 0000000000..e51edea345 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/MyDappsViewModel.java @@ -0,0 +1,17 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.service.AnalyticsServiceType; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class MyDappsViewModel extends BaseViewModel +{ + @Inject + MyDappsViewModel(AnalyticsServiceType analyticsService) + { + setAnalyticsService(analyticsService); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java index 6890595d3e..3accdb0ce8 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java @@ -201,4 +201,18 @@ public void onDestroy() if (disposable != null && !disposable.isDisposed()) disposable.dispose(); if (scriptUpdate != null && !scriptUpdate.isDisposed()) scriptUpdate.dispose(); } + + public boolean hasTokenScript(Token token) + { + return token != null && assetDefinitionService.getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address) != null; + } + + public void updateAttributes(Token token) + { + assetDefinitionService.refreshAllAttributes(token) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(b -> { }, this::onError) + .isDisposed(); + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NameThisWalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NameThisWalletViewModel.java index 9bfabf8ded..0888d9857c 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NameThisWalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NameThisWalletViewModel.java @@ -1,6 +1,8 @@ package com.alphawallet.app.viewmodel; +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; + import android.content.Context; import android.text.TextUtils; @@ -11,22 +13,20 @@ import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.repository.WalletItem; -import com.alphawallet.app.util.AWEnsResolver; -import com.alphawallet.app.util.EnsResolver; +import com.alphawallet.app.service.AnalyticsServiceType; +import com.alphawallet.app.util.ens.AWEnsResolver; +import com.alphawallet.app.util.ens.EnsResolver; import javax.annotation.Nullable; import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; import dagger.hilt.android.qualifiers.ApplicationContext; -import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; -import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; - @HiltViewModel public class NameThisWalletViewModel extends BaseViewModel { @@ -40,13 +40,16 @@ public class NameThisWalletViewModel extends BaseViewModel Disposable ensResolveDisposable; @Inject - NameThisWalletViewModel(GenericWalletInteract genericWalletInteract, @ApplicationContext Context context) + NameThisWalletViewModel( + GenericWalletInteract genericWalletInteract, + @ApplicationContext Context context, + AnalyticsServiceType analyticsService) { this.genericWalletInteract = genericWalletInteract; this.ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), context); + setAnalyticsService(analyticsService); } - @Override protected void onCleared() { @@ -107,7 +110,7 @@ public boolean checkEnsName(String newName, Realm.Transaction.OnSuccess onSucces if (!TextUtils.isEmpty(newName) && EnsResolver.isValidEnsName(newName)) { //does this new name correspond to ENS? - ensResolver.resolveENSAddress(newName, true) + ensResolver.resolveENSAddress(newName) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(addr -> checkAddress(addr, newName, onSuccess)) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NewSettingsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NewSettingsViewModel.java index 37ac4e09d8..b9573d16d7 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NewSettingsViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NewSettingsViewModel.java @@ -3,6 +3,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; + import android.content.Context; import com.alphawallet.app.entity.CurrencyItem; @@ -15,6 +16,8 @@ import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.router.ManageWalletsRouter; import com.alphawallet.app.router.MyAddressRouter; +import com.alphawallet.app.service.TickerService; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.TransactionsService; import com.alphawallet.app.util.LocaleUtils; @@ -26,7 +29,8 @@ import io.reactivex.Single; @HiltViewModel -public class NewSettingsViewModel extends BaseViewModel { +public class NewSettingsViewModel extends BaseViewModel +{ private final MutableLiveData defaultWallet = new MutableLiveData<>(); private final MutableLiveData transactions = new MutableLiveData<>(); @@ -38,6 +42,7 @@ public class NewSettingsViewModel extends BaseViewModel { private final LocaleRepositoryType localeRepository; private final CurrencyRepositoryType currencyRepository; private final TransactionsService transactionsService; + private final TickerService tickerService; @Inject NewSettingsViewModel( @@ -47,7 +52,10 @@ public class NewSettingsViewModel extends BaseViewModel { PreferenceRepositoryType preferenceRepository, LocaleRepositoryType localeRepository, CurrencyRepositoryType currencyRepository, - TransactionsService transactionsService) { + TransactionsService transactionsService, + TickerService tickerService, + AnalyticsServiceType analyticsService) + { this.genericWalletInteract = genericWalletInteract; this.myAddressRouter = myAddressRouter; this.manageWalletsRouter = manageWalletsRouter; @@ -55,32 +63,41 @@ public class NewSettingsViewModel extends BaseViewModel { this.localeRepository = localeRepository; this.currencyRepository = currencyRepository; this.transactionsService = transactionsService; + this.tickerService = tickerService; + setAnalyticsService(analyticsService); } - public ArrayList getLocaleList(Context context) { + public ArrayList getLocaleList(Context context) + { return localeRepository.getLocaleList(context); } - public void setLocale(Context activity) { + public void setLocale(Context activity) + { String currentLocale = localeRepository.getActiveLocale(); LocaleUtils.setLocale(activity, currentLocale); } - public void updateLocale(String newLocale, Context context) { + public void updateLocale(String newLocale, Context context) + { localeRepository.setUserPreferenceLocale(newLocale); localeRepository.setLocale(context, newLocale); } - public String getDefaultCurrency(){ + public String getDefaultCurrency() + { return currencyRepository.getDefaultCurrency(); } - public ArrayList getCurrencyList() { + public ArrayList getCurrencyList() + { return currencyRepository.getCurrencyList(); } - public Single updateCurrency(String currencyCode){ + public Single updateCurrency(String currencyCode) + { currencyRepository.setDefaultCurrency(currencyCode); + tickerService.updateCurrencyConversion(); //delete tickers from realm return transactionsService.wipeTickerData(); } @@ -90,7 +107,8 @@ public String getActiveLocale() return localeRepository.getActiveLocale(); } - public void showManageWallets(Context context, boolean clearStack) { + public void showManageWallets(Context context, boolean clearStack) + { manageWalletsRouter.open(context, clearStack); } @@ -98,32 +116,42 @@ public boolean getNotificationState() { return preferenceRepository.getNotificationsState(); } + public void setNotificationState(boolean notificationState) { preferenceRepository.setNotificationState(notificationState); } @Override - protected void onCleared() { + protected void onCleared() + { super.onCleared(); } - public LiveData defaultWallet() { + public LiveData defaultWallet() + { return defaultWallet; } - public LiveData transactions() { + public LiveData transactions() + { return transactions; } - public LiveData backUpMessage() { return backUpMessage; } - public void prepare() { + public LiveData backUpMessage() + { + return backUpMessage; + } + + public void prepare() + { disposable = genericWalletInteract .find() .subscribe(this::onDefaultWallet, this::onError); } - private void onDefaultWallet(Wallet wallet) { + private void onDefaultWallet(Wallet wallet) + { defaultWallet.setValue(wallet); TestWalletBackup(); @@ -138,7 +166,8 @@ public void TestWalletBackup() } } - public void showMyAddress(Context context) { + public void showMyAddress(Context context) + { myAddressRouter.open(context, defaultWallet.getValue()); } @@ -147,7 +176,8 @@ public void setIsDismissed(String walletAddr, boolean isDismissed) genericWalletInteract.setIsDismissed(walletAddr, isDismissed); } - public void setMarshMallowWarning(boolean shown) { + public void setMarshMallowWarning(boolean shown) + { preferenceRepository.setMarshMallowWarning(shown); } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/QrScannerViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/QrScannerViewModel.java new file mode 100644 index 0000000000..0da67f75eb --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/QrScannerViewModel.java @@ -0,0 +1,18 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.service.AnalyticsServiceType; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class QrScannerViewModel extends BaseViewModel +{ + + @Inject + public QrScannerViewModel(AnalyticsServiceType analyticsService) + { + setAnalyticsService(analyticsService); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/RedeemSignatureDisplayModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/RedeemSignatureDisplayModel.java index 0c02ad2755..f1cb0329d1 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/RedeemSignatureDisplayModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/RedeemSignatureDisplayModel.java @@ -1,27 +1,30 @@ package com.alphawallet.app.viewmodel; import android.app.Activity; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - import android.os.NetworkOnMainThreadException; + import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import com.alphawallet.app.entity.MessagePair; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.SignaturePair; -import com.alphawallet.app.entity.tokens.Ticket; -import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.TransferFromEventResponse; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.tokens.Ticket; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.interact.CreateTransactionInteract; import com.alphawallet.app.interact.FetchTokensInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.interact.MemPoolInteract; import com.alphawallet.app.interact.SignatureGenerateInteract; - +import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.KeyService; +import com.alphawallet.app.service.TokensService; +import com.alphawallet.token.entity.TicketRange; + import org.web3j.abi.datatypes.generated.Uint16; import org.web3j.utils.Numeric; @@ -30,6 +33,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; + import dagger.hilt.android.lifecycle.HiltViewModel; import io.reactivex.Observable; import io.reactivex.Single; @@ -37,12 +42,6 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import com.alphawallet.app.service.TokensService; -import com.alphawallet.token.entity.TicketRange; -import com.alphawallet.app.service.AssetDefinitionService; - -import javax.inject.Inject; - /** * Created by James on 25/01/2018. */ @@ -171,7 +170,7 @@ private void onToken(Token token) { this.token = token; - if (token != null && token.tokenInfo.address.equals(address) && token.hasArrayBalance()) + if (token != null && token.tokenInfo.address.equalsIgnoreCase(address) && token.hasArrayBalance()) { boolean allBurned = true; List balance = token.getArrayBalance(); @@ -290,7 +289,7 @@ private void onSignMessage(MessagePair pair, Wallet wallet) { //now run this guy through the signed message system if (pair != null) disposable = createTransactionInteract - .sign(wallet, pair, token.tokenInfo.chainId) + .sign(wallet, pair) .subscribe(this::onSignedMessage, this::onError); } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SelectNetworkFilterViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SelectNetworkFilterViewModel.java index 2723515ce3..6e2bf31043 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/SelectNetworkFilterViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SelectNetworkFilterViewModel.java @@ -1,10 +1,10 @@ package com.alphawallet.app.viewmodel; import com.alphawallet.app.entity.NetworkInfo; -import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; import com.alphawallet.app.repository.PreferenceRepositoryType; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.ui.widget.entity.NetworkItem; @@ -24,10 +24,12 @@ public class SelectNetworkFilterViewModel extends BaseViewModel { @Inject public SelectNetworkFilterViewModel(EthereumNetworkRepositoryType ethereumNetworkRepositoryType, TokensService tokensService, - PreferenceRepositoryType preferenceRepository) { + PreferenceRepositoryType preferenceRepository, + AnalyticsServiceType analyticsService) { this.networkRepository = ethereumNetworkRepositoryType; this.tokensService = tokensService; this.preferenceRepository = preferenceRepository; + setAnalyticsService(analyticsService); } public NetworkInfo[] getNetworkList() { @@ -107,4 +109,9 @@ public void removeCustomNetwork(long chainId) { public TokensService getTokensService() { return tokensService; } + + public List getActiveNetworks() + { + return networkRepository.getFilterNetworkList(); + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SelectRouteViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SelectRouteViewModel.java new file mode 100644 index 0000000000..4e2a7c5de5 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SelectRouteViewModel.java @@ -0,0 +1,119 @@ +package com.alphawallet.app.viewmodel; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.lifi.Route; +import com.alphawallet.app.repository.PreferenceRepositoryType; +import com.alphawallet.app.service.SwapService; +import com.alphawallet.app.ui.widget.entity.ProgressInfo; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +@HiltViewModel +public class SelectRouteViewModel extends BaseViewModel +{ + private final PreferenceRepositoryType preferenceRepository; + private final SwapService swapService; + private final MutableLiveData> routes = new MutableLiveData<>(); + private final MutableLiveData progressInfo = new MutableLiveData<>(); + private Disposable routeDisposable; + + @Inject + public SelectRouteViewModel( + PreferenceRepositoryType preferenceRepository, + SwapService swapService) + { + this.preferenceRepository = preferenceRepository; + this.swapService = swapService; + } + + public LiveData> routes() + { + return routes; + } + + public LiveData progressInfo() + { + return progressInfo; + } + + public void getRoutes(String fromChainId, + String toChainId, + String fromTokenAddress, + String toTokenAddress, + String fromAddress, + String fromAmount, + String slippage, + Set exchanges) + { + progressInfo.postValue(new ProgressInfo(true, R.string.message_fetching_routes)); + + routeDisposable = swapService + .getRoutes(fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAddress, fromAmount, slippage, exchanges) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onRoutes, this::onRoutesError); + } + + private void onRoutes(String result) + { + progressInfo.postValue(new ProgressInfo(false)); + + try + { + JSONObject obj = new JSONObject(result); + if (obj.has("routes")) + { + JSONArray json = obj.getJSONArray("routes"); + List routeList = new Gson().fromJson(json.toString(), new TypeToken>() + { + }.getType()); + routes.postValue(routeList); + } + else + { +// postError(C.ErrorCode.SWAP_CONNECTIONS_ERROR, result); + } + } + catch (JSONException e) + { +// postError(C.ErrorCode.SWAP_CONNECTIONS_ERROR, Objects.requireNonNull(e.getMessage())); + } + } + + private void onRoutesError(Throwable throwable) + { + // TODO: + } + + public Set getPreferredExchanges() + { + return preferenceRepository.getSelectedSwapProviders(); + } + + @Override + protected void onCleared() + { + if (routeDisposable != null && !routeDisposable.isDisposed()) + { + routeDisposable.dispose(); + } + super.onCleared(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SelectSwapProvidersViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SelectSwapProvidersViewModel.java new file mode 100644 index 0000000000..0c9c27222f --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SelectSwapProvidersViewModel.java @@ -0,0 +1,90 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.entity.lifi.SwapProvider; +import com.alphawallet.app.repository.PreferenceRepositoryType; +import com.alphawallet.app.repository.SwapRepositoryType; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import timber.log.Timber; + +@HiltViewModel +public class SelectSwapProvidersViewModel extends BaseViewModel +{ + private final PreferenceRepositoryType preferenceRepository; + private final SwapRepositoryType swapRepository; + + @Inject + public SelectSwapProvidersViewModel( + PreferenceRepositoryType preferenceRepository, + SwapRepositoryType swapRepository) + { + this.preferenceRepository = preferenceRepository; + this.swapRepository = swapRepository; + } + + public Set getPreferredExchanges() + { + Set exchanges = preferenceRepository.getSelectedSwapProviders(); + if (exchanges.isEmpty()) + { + List swapProviders = getSwapProviders(); + if (swapProviders != null) + { + for (SwapProvider provider : swapProviders) + { + exchanges.add(provider.key); + } + preferenceRepository.setSelectedSwapProviders(exchanges); + } + } + return exchanges; + } + + public List getSwapProviders() + { + List swapProviders = swapRepository.getProviders(); + + if (swapProviders != null) + { + Set preferredProviders = preferenceRepository.getSelectedSwapProviders(); + for (SwapProvider provider : swapProviders) + { + if (preferredProviders.contains(provider.key)) + { + provider.isChecked = true; + } + } + } + else + { + Timber.w("No Swap Providers found."); + swapProviders = new ArrayList<>(); + } + + return swapProviders; + } + + public boolean savePreferences(List swapProviders) + { + Set stringSet = new HashSet<>(); + for (SwapProvider providerool : swapProviders) + { + if (providerool.isChecked) + { + stringSet.add(providerool.key); + } + } + if (!stringSet.isEmpty()) + { + preferenceRepository.setSelectedSwapProviders(stringSet); + } + return !stringSet.isEmpty(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SellDetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SellDetailViewModel.java index ac276975bb..d4fbb66ccf 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/SellDetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SellDetailViewModel.java @@ -1,21 +1,21 @@ package com.alphawallet.app.viewmodel; import android.app.Activity; +import android.content.Context; + import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import android.content.Context; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.SignAuthenticationCallback; -import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; -import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.entity.tokendata.TokenTicker; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.interact.CreateTransactionInteract; import com.alphawallet.app.interact.FindDefaultNetworkInteract; -import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.router.SellDetailRouter; import com.alphawallet.app.service.AssetDefinitionService; @@ -130,7 +130,7 @@ public void generateUniversalLink(List ticketSendIndexList, String c //sign this link disposable = createTransactionInteract - .sign(defaultWallet().getValue(), tradeBytes, token.tokenInfo.chainId) + .sign(defaultWallet().getValue(), tradeBytes) .subscribe(this::gotSignature, this::onError); } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SendViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SendViewModel.java index 3ca0f220a6..174e31a57b 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/SendViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SendViewModel.java @@ -8,6 +8,7 @@ import com.alphawallet.app.C; import com.alphawallet.app.entity.AnalyticsProperties; +import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.TransactionData; @@ -17,20 +18,16 @@ import com.alphawallet.app.interact.CreateTransactionInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; -import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.router.MyAddressRouter; import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.GasService; import com.alphawallet.app.service.KeyService; -import com.alphawallet.app.util.RateApp; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.ui.ImportTokenActivity; import com.alphawallet.app.web3.entity.Web3Transaction; -import org.web3j.protocol.core.methods.response.EthEstimateGas; - import java.math.BigDecimal; import java.math.BigInteger; @@ -55,7 +52,6 @@ public class SendViewModel extends BaseViewModel { private final AssetDefinitionService assetDefinitionService; private final KeyService keyService; private final CreateTransactionInteract createTransactionInteract; - private final AnalyticsServiceType analyticsService; @Inject public SendViewModel(MyAddressRouter myAddressRouter, @@ -76,7 +72,7 @@ public SendViewModel(MyAddressRouter myAddressRouter, this.assetDefinitionService = assetDefinitionService; this.keyService = keyService; this.createTransactionInteract = createTransactionInteract; - this.analyticsService = analyticsService; + setAnalyticsService(analyticsService); } public MutableLiveData transactionFinalised() @@ -107,7 +103,7 @@ public void showImportLink(Context context, String importTxt) public void fetchToken(long chainId, String address, String walletAddress) { - tokensService.update(address, chainId) + tokensService.update(address, chainId, ContractType.NOT_SET) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(tokenInfo -> gotTokenUpdate(tokenInfo, walletAddress), this::onError).isDisposed(); @@ -173,12 +169,4 @@ public void sendTransaction(Web3Transaction finalTx, Wallet wallet, long chainId .subscribe(transactionFinalised::postValue, transactionError::postValue); } - - public void actionSheetConfirm(String mode) - { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(mode); - - analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); - } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SignDialogViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SignDialogViewModel.java new file mode 100644 index 0000000000..c2067ff8aa --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SignDialogViewModel.java @@ -0,0 +1,112 @@ +package com.alphawallet.app.viewmodel; + +import android.app.Activity; + +import androidx.core.util.Pair; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.SignAuthenticationCallback; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; +import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.repository.PreferenceRepositoryType; +import com.alphawallet.app.repository.TransactionRepositoryType; +import com.alphawallet.app.service.KeyService; +import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; +import com.alphawallet.token.entity.Signable; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * Created by JB on 22/11/2022. + */ +@HiltViewModel +public class SignDialogViewModel extends BaseViewModel +{ + private final PreferenceRepositoryType preferenceRepository; + private final TransactionRepositoryType transactionRepositoryType; + private final KeyService keyService; + private final GenericWalletInteract walletInteract; + private final MutableLiveData completed = new MutableLiveData<>(false); + private final MutableLiveData> message = new MutableLiveData<>(); + private Wallet wallet; + + @Inject + public SignDialogViewModel( + PreferenceRepositoryType preferenceRepository, + GenericWalletInteract walletInteract, + TransactionRepositoryType transactionRepositoryType, + KeyService keyService) + { + this.preferenceRepository = preferenceRepository; + this.transactionRepositoryType = transactionRepositoryType; + this.keyService = keyService; + this.walletInteract = walletInteract; + + disposable = walletInteract.find() + .observeOn(Schedulers.io()) + .subscribeOn(Schedulers.io()) + .subscribe(w -> wallet = w, this::onError); + } + + public LiveData completed() + { + return completed; + } + + public LiveData> message() + { + return message; + } + + private void compareToActiveWallet(String signingAddress) + { + String activeWallet = preferenceRepository.getCurrentWalletAddress(); + if (!activeWallet.equalsIgnoreCase(signingAddress)) + { + message.postValue(new Pair<>(R.string.message_wc_wallet_different_from_active_wallet, R.drawable.ic_red_warning)); + } + } + + public void getAuthentication(Activity activity, SignAuthenticationCallback sCallback) + { + keyService.getAuthenticationForSignature(wallet, activity, sCallback); + } + + public void signMessage(Signable message, ActionSheetCallback aCallback) + { + disposable = transactionRepositoryType.getSignature(wallet, message) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(sig -> signComplete(sig, message, aCallback), + error -> signFailed(error, message, aCallback)); + } + + private void signComplete(SignatureFromKey signature, Signable message, ActionSheetCallback aCallback) + { + aCallback.signingComplete(signature, message); + completed.postValue(true); + } + + private void signFailed(Throwable error, Signable message, ActionSheetCallback aCallback) + { + aCallback.signingFailed(error, message); + completed.postValue(false); + } + + public void setSigningWallet(String account) + { + disposable = walletInteract.findWallet(account) + .observeOn(Schedulers.io()) + .subscribeOn(Schedulers.io()) + .subscribe(w -> wallet = w, this::onError); // TODO: If wallet not found then report error to user rather than trying to sign on default wallet + + compareToActiveWallet(account); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SplashViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SplashViewModel.java index af79377672..d3cd360f48 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/SplashViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SplashViewModel.java @@ -8,7 +8,6 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; import com.alphawallet.app.entity.CreateWalletCallbackInterface; import com.alphawallet.app.entity.Operation; @@ -16,6 +15,7 @@ import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.interact.FetchWalletsInteract; import com.alphawallet.app.repository.PreferenceRepositoryType; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.KeyService; import java.io.File; @@ -28,7 +28,7 @@ import io.reactivex.schedulers.Schedulers; @HiltViewModel -public class SplashViewModel extends ViewModel +public class SplashViewModel extends BaseViewModel { private static final String LEGACY_CERTIFICATE_DB = "CERTIFICATE_CACHE-db.realm"; private static final String LEGACY_AUX_DB_PREFIX = "AuxData-"; @@ -43,11 +43,13 @@ public class SplashViewModel extends ViewModel @Inject SplashViewModel(FetchWalletsInteract fetchWalletsInteract, PreferenceRepositoryType preferenceRepository, - KeyService keyService) { + KeyService keyService, + AnalyticsServiceType analyticsService) + { this.fetchWalletsInteract = fetchWalletsInteract; this.preferenceRepository = preferenceRepository; this.keyService = keyService; - + setAnalyticsService(analyticsService); // increase launch count // this.preferenceRepository.incrementLaunchCount(); } @@ -61,14 +63,19 @@ public void fetchWallets() } //on wallet error ensure execution still continues and splash screen terminates - private void onError(Throwable throwable) { + @Override + protected void onError(Throwable throwable) + { wallets.postValue(new Wallet[0]); } - public LiveData wallets() { + public LiveData wallets() + { return wallets; } - public LiveData createWallet() { + + public LiveData createWallet() + { return createWallet; } @@ -81,21 +88,6 @@ public void createNewWallet(Activity ctx, CreateWalletCallbackInterface createCa .isDisposed(); } - private String stripFilename(String name) - { - int index = name.lastIndexOf(".apk"); - if (index > 0) - { - name = name.substring(0, index); - } - index = name.lastIndexOf("-"); - if (index > 0) - { - name = name.substring(index+1); - } - return name; - } - public void StoreHDKey(String address, KeyService.AuthenticationLevel authLevel) { if (!address.equals(ZERO_ADDRESS)) @@ -104,7 +96,10 @@ public void StoreHDKey(String address, KeyService.AuthenticationLevel authLevel) wallet.type = WalletType.HDKEY; wallet.authLevel = authLevel; fetchWalletsInteract.storeWallet(wallet) - .map(w -> { preferenceRepository.setCurrentWalletAddress(w.address); return w; }) + .map(w -> { + preferenceRepository.setCurrentWalletAddress(w.address); + return w; + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(newWallet -> wallets.postValue(new Wallet[]{newWallet}), this::onError).isDisposed(); @@ -113,6 +108,8 @@ public void StoreHDKey(String address, KeyService.AuthenticationLevel authLevel) { wallets.postValue(new Wallet[0]); } + + preferenceRepository.setNewWallet(address, true); } public void completeAuthentication(Operation taskCode) @@ -165,11 +162,13 @@ public void setDefaultBrowser() preferenceRepository.setActiveBrowserNetwork(MAINNET_ID); } - public long getInstallTime() { + public long getInstallTime() + { return preferenceRepository.getInstallTime(); } - public void setInstallTime(long time) { + public void setInstallTime(long time) + { preferenceRepository.setInstallTime(time); } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SupportSettingsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SupportSettingsViewModel.java new file mode 100644 index 0000000000..160385ddc1 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SupportSettingsViewModel.java @@ -0,0 +1,17 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.service.AnalyticsServiceType; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupportSettingsViewModel extends BaseViewModel +{ + @Inject + SupportSettingsViewModel(AnalyticsServiceType analyticsService) + { + setAnalyticsService(analyticsService); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/SwapViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/SwapViewModel.java index 73f1eea7f9..adf2f00e32 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/SwapViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/SwapViewModel.java @@ -1,11 +1,14 @@ package com.alphawallet.app.viewmodel; import android.app.Activity; +import android.content.Intent; +import androidx.activity.result.ActivityResultLauncher; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.alphawallet.app.C; +import com.alphawallet.app.R; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.TransactionData; @@ -13,12 +16,19 @@ import com.alphawallet.app.entity.lifi.Chain; import com.alphawallet.app.entity.lifi.Connection; import com.alphawallet.app.entity.lifi.Quote; -import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.entity.lifi.SwapProvider; +import com.alphawallet.app.entity.lifi.Token; import com.alphawallet.app.interact.CreateTransactionInteract; +import com.alphawallet.app.repository.PreferenceRepositoryType; +import com.alphawallet.app.repository.SwapRepositoryType; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.KeyService; import com.alphawallet.app.service.SwapService; import com.alphawallet.app.service.TokensService; +import com.alphawallet.app.ui.SelectRouteActivity; +import com.alphawallet.app.ui.SelectSwapProvidersActivity; +import com.alphawallet.app.ui.widget.entity.ProgressInfo; import com.alphawallet.app.util.BalanceUtils; import com.alphawallet.app.util.Hex; import com.alphawallet.app.web3.entity.Address; @@ -33,6 +43,9 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; import javax.inject.Inject; @@ -40,11 +53,14 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; @HiltViewModel public class SwapViewModel extends BaseViewModel { private final AssetDefinitionService assetDefinitionService; + private final PreferenceRepositoryType preferenceRepository; + private final SwapRepositoryType swapRepository; private final TokensService tokensService; private final SwapService swapService; private final CreateTransactionInteract createTransactionInteract; @@ -55,7 +71,7 @@ public class SwapViewModel extends BaseViewModel private final MutableLiveData> connections = new MutableLiveData<>(); private final MutableLiveData quote = new MutableLiveData<>(); private final MutableLiveData network = new MutableLiveData<>(); - private final MutableLiveData progressInfo = new MutableLiveData<>(); + private final MutableLiveData progressInfo = new MutableLiveData<>(); private final MutableLiveData transactionFinalised = new MutableLiveData<>(); private final MutableLiveData transactionError = new MutableLiveData<>(); @@ -67,16 +83,22 @@ public class SwapViewModel extends BaseViewModel @Inject public SwapViewModel( AssetDefinitionService assetDefinitionService, + PreferenceRepositoryType preferenceRepository, + SwapRepositoryType swapRepository, TokensService tokensService, SwapService swapService, CreateTransactionInteract createTransactionInteract, - KeyService keyService) + KeyService keyService, + AnalyticsServiceType analyticsService) { this.assetDefinitionService = assetDefinitionService; + this.preferenceRepository = preferenceRepository; + this.swapRepository = swapRepository; this.tokensService = tokensService; this.swapService = swapService; this.createTransactionInteract = createTransactionInteract; this.keyService = keyService; + setAnalyticsService(analyticsService); } public AssetDefinitionService getAssetDefinitionService() @@ -114,7 +136,7 @@ public LiveData network() return network; } - public LiveData progressInfo() + public LiveData progressInfo() { return progressInfo; } @@ -141,37 +163,49 @@ public void setChain(Chain c) public void getChains() { - progressInfo.postValue(C.ProgressInfo.FETCHING_CHAINS); - progress.postValue(true); + progressInfo.postValue(new ProgressInfo(true, R.string.message_fetching_chains)); chainsDisposable = swapService.getChains() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onChains, this::onError); + .subscribe(this::onChains, this::onChainsError); + } + + public String getSwapProviderUrl(String key) + { + List tools = getSwapProviders(); + for (SwapProvider td : tools) + { + if (key.startsWith(td.key)) + { + return td.url; + } + } + + return ""; } public void getConnections(long from, long to) { - progressInfo.postValue(C.ProgressInfo.FETCHING_CONNECTIONS); - progress.postValue(true); + progressInfo.postValue(new ProgressInfo(true, R.string.message_fetching_connections)); connectionsDisposable = swapService.getConnections(from, to) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onConnections, this::onError); + .subscribe(this::onConnections, this::onConnectionsError); } - public void getQuote(Connection.LToken source, Connection.LToken dest, String address, String amount, String slippage) + public void getQuote(Token source, Token dest, String address, String amount, String slippage, String allowExchanges) { - if (hasEnoughBalance(address, source, amount)) + if (!isValidAmount(amount)) return; + if (hasEnoughBalance(source, amount)) { - progressInfo.postValue(C.ProgressInfo.FETCHING_QUOTE); - progress.postValue(true); + progressInfo.postValue(new ProgressInfo(true, R.string.message_fetching_quote)); - quoteDisposable = swapService.getQuote(source, dest, address, amount, slippage) + quoteDisposable = swapService.getQuote(source, dest, address, amount, slippage, allowExchanges) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onQuote, this::onError); + .subscribe(this::onQuote, this::onQuoteError); } else { @@ -179,9 +213,37 @@ public void getQuote(Connection.LToken source, Connection.LToken dest, String ad } } - public boolean hasEnoughBalance(String address, Connection.LToken source, String amount) + private boolean isValidAmount(String amount) + { + try + { + BigDecimal d = new BigDecimal(amount); + } + catch (Exception e) + { + return false; + } + return true; + } + + private void onChainsError(Throwable t) { - BigDecimal bal = new BigDecimal(getBalance(address, source)); + postError(C.ErrorCode.SWAP_CHAIN_ERROR, Objects.requireNonNull(t.getMessage())); + } + + private void onConnectionsError(Throwable t) + { + postError(C.ErrorCode.SWAP_CONNECTIONS_ERROR, Objects.requireNonNull(t.getMessage())); + } + + private void onQuoteError(Throwable t) + { + postError(C.ErrorCode.SWAP_QUOTE_ERROR, Objects.requireNonNull(t.getMessage())); + } + + public boolean hasEnoughBalance(Token source, String amount) + { + BigDecimal bal = new BigDecimal(getBalance(source)); BigDecimal reqAmount = new BigDecimal(amount); return bal.compareTo(reqAmount) >= 0; } @@ -203,12 +265,12 @@ private void onChains(String result) } else { - error.postValue(new ErrorEnvelope(C.ErrorCode.SWAP_API_ERROR, result)); + postError(C.ErrorCode.SWAP_CHAIN_ERROR, result); } } catch (JSONException e) { - error.postValue(new ErrorEnvelope(C.ErrorCode.SWAP_API_ERROR, e.getMessage())); + postError(C.ErrorCode.SWAP_CHAIN_ERROR, Objects.requireNonNull(e.getMessage())); } } @@ -229,22 +291,22 @@ private void onConnections(String result) } else { - error.postValue(new ErrorEnvelope(C.ErrorCode.SWAP_API_ERROR, result)); + postError(C.ErrorCode.SWAP_CONNECTIONS_ERROR, result); } } catch (JSONException e) { - error.postValue(new ErrorEnvelope(C.ErrorCode.SWAP_API_ERROR, e.getMessage())); + postError(C.ErrorCode.SWAP_CONNECTIONS_ERROR, Objects.requireNonNull(e.getMessage())); } - progress.postValue(false); + progressInfo.postValue(new ProgressInfo(false)); } private void onQuote(String result) { if (!isValidQuote(result)) { - error.postValue(new ErrorEnvelope(C.ErrorCode.SWAP_API_ERROR, result)); + postError(C.ErrorCode.SWAP_QUOTE_ERROR, result); } else { @@ -252,7 +314,35 @@ private void onQuote(String result) quote.postValue(q); } - progress.postValue(false); + progressInfo.postValue(new ProgressInfo(false)); + } + + private void postError(int errorCode, String errorStr) + { + Timber.e(errorStr); + if (errorStr.toLowerCase(Locale.ENGLISH).contains("timeout")) + { + this.error.postValue(new ErrorEnvelope(C.ErrorCode.SWAP_TIMEOUT_ERROR, errorStr)); + return; + } + this.error.postValue(new ErrorEnvelope(errorCode, checkMessage(errorStr))); + } + + private String checkMessage(String errorStr) + { + try + { + JSONObject json = new JSONObject(errorStr); + if (json.has("message")) + { + return json.getString("message"); + } + } + catch (JSONException e) + { + Timber.e(e); + } + return errorStr; } private boolean isValidQuote(String result) @@ -262,18 +352,18 @@ private boolean isValidQuote(String result) && result.contains("tool"); } - public String getBalance(String walletAddress, Connection.LToken token) + public String getBalance(Token token) { - String address = token.address; - - // Note: In the LIFI API, the native token has either of these two addresses. - // In AlphaWallet, the wallet address is used. - if (address.equalsIgnoreCase("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") || - address.equalsIgnoreCase("0x0000000000000000000000000000000000000000")) + com.alphawallet.app.entity.tokens.Token t; + if (token.isNativeToken()) { - address = walletAddress; + t = tokensService.getServiceToken(token.chainId); } - Token t = tokensService.getToken(token.chainId, address); + else + { + t = tokensService.getToken(token.chainId, token.address); + } + if (t != null) { return BalanceUtils.getShortFormat(t.balance.toString(), t.tokenInfo.decimals); @@ -314,6 +404,16 @@ public Web3Transaction buildWeb3Transaction(Quote quote) ); } + public List getSwapProviders() + { + return swapRepository.getProviders(); + } + + public Set getPreferredSwapProviders() + { + return preferenceRepository.getSelectedSwapProviders(); + } + @Override protected void onCleared() { @@ -335,4 +435,48 @@ protected void onCleared() } super.onCleared(); } + + public void getRoutes(Activity activity, + ActivityResultLauncher launcher, + Token source, + Token dest, + String address, + String amount, + String slippage) + { + if (!isValidAmount(amount)) return; + if (hasEnoughBalance(source, amount)) + { + Intent intent = new Intent(activity, SelectRouteActivity.class); + intent.putExtra("fromChainId", String.valueOf(source.chainId)); + intent.putExtra("toChainId", String.valueOf(dest.chainId)); + intent.putExtra("fromTokenAddress", String.valueOf(source.address)); + intent.putExtra("toTokenAddress", String.valueOf(dest.address)); + intent.putExtra("fromAddress", address); + intent.putExtra("fromAmount", BalanceUtils.getRawFormat(amount, source.decimals)); + intent.putExtra("fromTokenDecimals", source.decimals); + intent.putExtra("slippage", slippage); + intent.putExtra("fromTokenSymbol", source.symbol); + intent.putExtra("fromTokenIcon", source.symbol); + intent.putExtra("fromTokenLogoUri", source.logoURI); + launcher.launch(intent); + } + else + { + error.postValue(new ErrorEnvelope(C.ErrorCode.INSUFFICIENT_BALANCE, "")); + } + } + + public void prepare(Activity activity, ActivityResultLauncher launcher) + { + if (getPreferredSwapProviders().isEmpty()) + { + Intent intent = new Intent(activity, SelectSwapProvidersActivity.class); + launcher.launch(intent); + } + else + { + getChains(); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java index aabc6fb675..627a563a0e 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java @@ -13,8 +13,8 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.AnalyticsProperties; -import com.alphawallet.app.entity.DAppFunction; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.Transaction; @@ -50,11 +50,11 @@ import com.alphawallet.app.util.Utils; import com.alphawallet.app.web3.entity.Address; import com.alphawallet.app.web3.entity.Web3Transaction; +import com.alphawallet.token.entity.Attribute; import com.alphawallet.token.entity.ContractAddress; import com.alphawallet.token.entity.FunctionDefinition; import com.alphawallet.token.entity.MethodArg; import com.alphawallet.token.entity.SigReturnType; -import com.alphawallet.token.entity.Signable; import com.alphawallet.token.entity.TSAction; import com.alphawallet.token.entity.TicketRange; import com.alphawallet.token.entity.TokenScriptResult; @@ -74,6 +74,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.inject.Inject; @@ -90,7 +91,8 @@ * Stormbird in Singapore */ @HiltViewModel -public class TokenFunctionViewModel extends BaseViewModel { +public class TokenFunctionViewModel extends BaseViewModel +{ private final AssetDefinitionService assetDefinitionService; private final CreateTransactionInteract createTransactionInteract; private final GasService gasService; @@ -100,7 +102,6 @@ public class TokenFunctionViewModel extends BaseViewModel { private final GenericWalletInteract genericWalletInteract; private final OpenSeaService openseaService; private final FetchTransactionsInteract fetchTransactionsInteract; - private final AnalyticsServiceType analyticsService; private final MutableLiveData insufficientFunds = new MutableLiveData<>(); private final MutableLiveData invalidAddress = new MutableLiveData<>(); private final MutableLiveData sig = new MutableLiveData<>(); @@ -110,7 +111,6 @@ public class TokenFunctionViewModel extends BaseViewModel { private final MutableLiveData transactionError = new MutableLiveData<>(); private final MutableLiveData gasEstimateComplete = new MutableLiveData<>(); private final MutableLiveData> traits = new MutableLiveData<>(); - private final MutableLiveData attrs = new MutableLiveData<>(); private final MutableLiveData assetContract = new MutableLiveData<>(); private final MutableLiveData nftAsset = new MutableLiveData<>(); private final MutableLiveData scriptUpdateInProgress = new MutableLiveData<>(); @@ -151,7 +151,7 @@ public class TokenFunctionViewModel extends BaseViewModel { this.genericWalletInteract = genericWalletInteract; this.openseaService = openseaService; this.fetchTransactionsInteract = fetchTransactionsInteract; - this.analyticsService = analyticsService; + setAnalyticsService(analyticsService); } public AssetDefinitionService getAssetDefinitionService() @@ -184,7 +184,10 @@ public LiveData newScriptFound() return newScriptFound; } - public LiveData scriptUpdateInProgress() { return scriptUpdateInProgress; } + public LiveData scriptUpdateInProgress() + { + return scriptUpdateInProgress; + } public MutableLiveData transactionFinalised() { @@ -294,15 +297,6 @@ private void onSigCheckError(Throwable throwable) sig.postValue(failSig); } - public void signMessage(Signable message, DAppFunction dAppFunction, long chainId) - { - disposable = createTransactionInteract.sign(wallet, message, chainId) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(sig -> dAppFunction.DAppReturn(sig.signature, message), - error -> dAppFunction.DAppError(error, message)); - } - public String getTransactionBytes(Token token, BigInteger tokenId, FunctionDefinition def) { return assetDefinitionService.generateTransactionPayload(token, tokenId, def); @@ -497,7 +491,7 @@ else if (action.function.contract.addresses.get(token.tokenInfo.chainId) != null to = action.function.contract.addresses.get(token.tokenInfo.chainId).get(0); } - if (to == null || !Utils.isAddressValid(to)) + if (!Utils.isAddressValid(to)) { invalidAddress.postValue(to); isValid = false; @@ -709,10 +703,9 @@ public void sendTransaction(Web3Transaction finalTx, long chainId, String overri public void actionSheetConfirm(String mode) { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(mode); - - analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); + AnalyticsProperties props = new AnalyticsProperties(); + props.put(Analytics.PROPS_ACTION_SHEET_MODE, mode); + track(Analytics.Action.ACTION_SHEET_COMPLETED, props); } public Single showTransferSelectCount(Context ctx, Token token, BigInteger tokenId) @@ -870,7 +863,7 @@ private void onAsset(String result, Token token, BigInteger tokenId) } else { - storeAsset(token, tokenId, new NFTAsset(result), oldAsset); + storeAsset(token, tokenId, asset, oldAsset); asset.attachOpenSeaAssetData(osAsset); nftAsset.postValue(asset); } @@ -892,4 +885,41 @@ private void onAsset(String result, Token token, BigInteger tokenId) getTokenMetadata(token, tokenId, oldAsset); } } + + public String getBrowserRPC(long chainId) + { + return ethereumNetworkRepository.getDappBrowserRPC(chainId); + } + + public boolean hasTokenScript(Token token) + { + return token != null && assetDefinitionService.getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address) != null; + } + + public void updateLocalAttributes(Token token, BigInteger tokenId) + { + //Fetch Allowed attributes, then call updateAllowedAttributes + assetDefinitionService.fetchFunctionMap(token, Collections.singletonList(tokenId)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(availableActions -> updateAllowedAttrs(token, availableActions), this::onError) + .isDisposed(); + } + + private void updateAllowedAttrs(Token token, Map> availableActions) + { + if (!availableActions.keySet().stream().findFirst().isPresent()) + { + return; + } + TokenDefinition td = assetDefinitionService.getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + List localAttrList = assetDefinitionService.getLocalAttributes(td, availableActions); + + //now refresh all these attrs + assetDefinitionService.refreshAttributes(token, td, availableActions.keySet().stream().findFirst().get(), localAttrList) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> { }, this::onError) + .isDisposed(); + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/Tokens.java b/app/src/main/java/com/alphawallet/app/viewmodel/Tokens.java new file mode 100644 index 0000000000..07689b8646 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/Tokens.java @@ -0,0 +1,48 @@ +package com.alphawallet.app.viewmodel; + +import com.alphawallet.app.entity.lifi.Token; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +public class Tokens +{ + public static void sortValue(List tokenItems) + { + Collections.sort(tokenItems, (l, r) -> { + if (l.isNativeToken()) + { + return -1; + } + else if (r.isNativeToken()) + { + return 1; + } + else + { + BigDecimal lBal = new BigDecimal(l.fiatEquivalent); + BigDecimal rBal = new BigDecimal(r.fiatEquivalent); + return rBal.compareTo(lBal); + } + }); + } + + public static void sortName(List tokenItems) + { + Collections.sort(tokenItems, (l, r) -> { + if (l.isNativeToken()) + { + return -1; + } + else if (r.isNativeToken()) + { + return 1; + } + else + { + return l.name.trim().compareToIgnoreCase(r.name.trim()); + } + }); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java index 8e27b14b8d..e83351c178 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java @@ -1,5 +1,7 @@ package com.alphawallet.app.viewmodel; +import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; + import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -9,9 +11,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.alphawallet.app.C; import com.alphawallet.app.R; -import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.SignAuthenticationCallback; @@ -43,6 +43,8 @@ import java.math.RoundingMode; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; + import dagger.hilt.android.lifecycle.HiltViewModel; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -50,10 +52,6 @@ import io.reactivex.schedulers.Schedulers; import io.realm.Realm; -import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; - -import javax.inject.Inject; - @HiltViewModel public class TransactionDetailViewModel extends BaseViewModel { private final ExternalBrowserRouter externalBrowserRouter; @@ -66,7 +64,6 @@ public class TransactionDetailViewModel extends BaseViewModel { private final KeyService keyService; private final TokensService tokenService; private final GasService gasService; - private final AnalyticsServiceType analyticsService; private final MutableLiveData latestBlock = new MutableLiveData<>(); private final MutableLiveData latestTx = new MutableLiveData<>(); @@ -107,7 +104,7 @@ public LiveData latestBlock() { this.keyService = keyService; this.gasService = gasService; this.createTransactionInteract = createTransactionInteract; - this.analyticsService = analyticsService; + setAnalyticsService(analyticsService); } public MutableLiveData transactionFinalised() @@ -130,7 +127,8 @@ public void prepare(final long chainId, final String walletAddr) public void showMoreDetails(Context context, Transaction transaction) { Uri uri = buildEtherscanUri(transaction); - if (uri != null && Utils.isValidUrl(uri.toString())) { + if (uri != null) + { externalBrowserRouter.open(context, uri); } } @@ -160,7 +158,8 @@ private void displayCurrentPendingTime(final String txHash) public void shareTransactionDetail(Context context, Transaction transaction) { Uri shareUri = buildEtherscanUri(transaction); - if (shareUri != null) { + if (shareUri != null) + { Intent sharingIntent = new Intent(Intent.ACTION_SEND); sharingIntent.setType("text/plain"); sharingIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.subject_transaction_detail)); @@ -178,10 +177,14 @@ public Token getToken(long chainId, String address) private Uri buildEtherscanUri(Transaction transaction) { NetworkInfo networkInfo = networkInteract.getNetworkInfo(transaction.chainId); - if (networkInfo != null) { + if (networkInfo != null && Utils.isValidUrl(networkInfo.etherscanUrl)) + { return networkInfo.getEtherscanUri(transaction.hash); } - return null; + else + { + return null; + } } public boolean hasEtherscanDetail(Transaction tx) @@ -303,12 +306,4 @@ public void sendTransaction(Web3Transaction finalTx, Wallet wallet, long chainId .subscribe(transactionFinalised::postValue, transactionError::postValue); } - - public void actionSheetConfirm(String mode) - { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(mode); - - analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); - } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TransferTicketDetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TransferTicketDetailViewModel.java index 0e8f662119..bb9a1e16ce 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TransferTicketDetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TransferTicketDetailViewModel.java @@ -9,11 +9,9 @@ import androidx.lifecycle.MutableLiveData; import com.alphawallet.app.C; -import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.DisplayState; -import com.alphawallet.app.entity.GasSettings; import com.alphawallet.app.entity.Operation; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.TransactionData; @@ -38,8 +36,6 @@ import com.alphawallet.token.entity.SignableBytes; import com.alphawallet.token.tools.ParseMagicLink; -import org.web3j.protocol.core.methods.response.EthEstimateGas; - import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; @@ -56,7 +52,8 @@ * Created by James on 21/02/2018. */ @HiltViewModel -public class TransferTicketDetailViewModel extends BaseViewModel { +public class TransferTicketDetailViewModel extends BaseViewModel +{ private final MutableLiveData defaultWallet = new MutableLiveData<>(); private final MutableLiveData newTransaction = new MutableLiveData<>(); private final MutableLiveData universalLinkReady = new MutableLiveData<>(); @@ -69,7 +66,6 @@ public class TransferTicketDetailViewModel extends BaseViewModel { private final FetchTransactionsInteract fetchTransactionsInteract; private final AssetDefinitionService assetDefinitionService; private final GasService gasService; - private final AnalyticsServiceType analyticsService; private final TokensService tokensService; private ParseMagicLink parser; @@ -85,29 +81,43 @@ public class TransferTicketDetailViewModel extends BaseViewModel { AssetDefinitionService assetDefinitionService, GasService gasService, AnalyticsServiceType analyticsService, - TokensService tokensService) { + TokensService tokensService) + { this.genericWalletInteract = genericWalletInteract; this.keyService = keyService; this.createTransactionInteract = createTransactionInteract; this.fetchTransactionsInteract = fetchTransactionsInteract; this.assetDefinitionService = assetDefinitionService; this.gasService = gasService; - this.analyticsService = analyticsService; this.tokensService = tokensService; + setAnalyticsService(analyticsService); } public MutableLiveData transactionFinalised() { return transactionFinalised; } - public MutableLiveData transactionError() { return transactionError; } + + public MutableLiveData transactionError() + { + return transactionError; + } public LiveData defaultWallet() { return defaultWallet; } - public LiveData newTransaction() { return newTransaction; } - public LiveData universalLinkReady() { return universalLinkReady; } + + public LiveData newTransaction() + { + return newTransaction; + } + + public LiveData universalLinkReady() + { + return universalLinkReady; + } + private void initParser() { if (parser == null) @@ -126,13 +136,14 @@ public void prepare(Token token) gasService.startGasPriceCycle(token.tokenInfo.chainId); } - private void onDefaultWallet(Wallet wallet) { + private void onDefaultWallet(Wallet wallet) + { defaultWallet.setValue(wallet); } public Wallet getWallet() { - return defaultWallet.getValue(); + return defaultWallet.getValue(); } public void setWallet(Wallet wallet) @@ -162,7 +173,7 @@ public void generateUniversalLink(List ticketSendIndexList, String c //sign this link disposable = createTransactionInteract - .sign(defaultWallet().getValue(), tradeBytes, token.tokenInfo.chainId) + .sign(defaultWallet().getValue(), tradeBytes) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::gotSignature, this::onError); @@ -183,7 +194,7 @@ public void generateSpawnLink(List tokenIds, String contractAddress, //sign this link disposable = createTransactionInteract - .sign(defaultWallet().getValue(), tradeBytes, token.tokenInfo.chainId) + .sign(defaultWallet().getValue(), tradeBytes) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::gotSignature, this::onError); @@ -210,7 +221,7 @@ public void createTokenTransfer(String to, Token token, List transfe { final byte[] data = TokenRepository.createTicketTransferData(to, transferList, token); disposable = createTransactionInteract.create(defaultWallet.getValue(), token.getAddress(), - BigInteger.ZERO, gasService.getGasPrice(), new BigInteger(C.DEFAULT_GAS_LIMIT_FOR_NONFUNGIBLE_TOKENS), data, token.tokenInfo.chainId) + BigInteger.ZERO, gasService.getGasPrice(), new BigInteger(C.DEFAULT_GAS_LIMIT_FOR_NONFUNGIBLE_TOKENS), data, token.tokenInfo.chainId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(newTransaction::postValue, this::onError); @@ -289,21 +300,15 @@ public void sendTransaction(Web3Transaction finalTx, Wallet wallet, long chainId transactionError::postValue); } - public byte[] getERC721TransferBytes(String to, String contractAddress, String tokenId, long chainId) { + public byte[] getERC721TransferBytes(String to, String contractAddress, String tokenId, long chainId) + { Token token = tokensService.getToken(chainId, contractAddress); List tokenIds = token.stringHexToBigIntegerList(tokenId); return TokenRepository.createERC721TransferFunction(to, token, tokenIds); } - public void actionSheetConfirm(String mode) + public TokensService getTokenService() { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData(mode); - - analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); - } - - public TokensService getTokenService() { return tokensService; } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectV2ViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectV2ViewModel.java new file mode 100644 index 0000000000..d043a4a1e8 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectV2ViewModel.java @@ -0,0 +1,66 @@ +package com.alphawallet.app.viewmodel; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.interact.FetchWalletsInteract; +import com.alphawallet.app.interact.GenericWalletInteract; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class WalletConnectV2ViewModel extends BaseViewModel +{ + private final MutableLiveData wallets = new MutableLiveData<>(); + private final MutableLiveData defaultWallet = new MutableLiveData<>(); + + private final FetchWalletsInteract fetchWalletsInteract; + private final GenericWalletInteract genericWalletInteract; + + @Inject + WalletConnectV2ViewModel(FetchWalletsInteract fetchWalletsInteract, + GenericWalletInteract genericWalletInteract) + { + this.fetchWalletsInteract = fetchWalletsInteract; + this.genericWalletInteract = genericWalletInteract; + fetchWallets(); + fetchDefaultWallet(); + } + + public LiveData wallets() + { + return wallets; + } + + public LiveData defaultWallet() + { + return defaultWallet; + } + + public void fetchDefaultWallet() + { + disposable = genericWalletInteract + .find() + .subscribe(this::onDefaultWallet, this::onError); + } + + private void onDefaultWallet(Wallet wallet) + { + this.defaultWallet.postValue(wallet); + } + + public void fetchWallets() + { + disposable = fetchWalletsInteract + .fetch() + .subscribe(this::onWallets, this::onError); + } + + private void onWallets(Wallet[] wallets) + { + this.wallets.postValue(wallets); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java index 9d2a037cd1..4a3b6b118e 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java @@ -3,6 +3,7 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; import android.app.Activity; +import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -14,17 +15,20 @@ import androidx.lifecycle.MutableLiveData; import com.alphawallet.app.C; -import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.DAppFunction; +import com.alphawallet.app.entity.GenericCallback; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.SendTransactionInterface; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletConnectActions; import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.entity.walletconnect.WalletConnectV2SessionItem; import com.alphawallet.app.interact.CreateTransactionInteract; +import com.alphawallet.app.interact.FetchWalletsInteract; import com.alphawallet.app.interact.FindDefaultNetworkInteract; import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.interact.WalletConnectInteract; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; import com.alphawallet.app.repository.SignRecord; import com.alphawallet.app.repository.entity.RealmWCSession; @@ -35,6 +39,7 @@ import com.alphawallet.app.service.RealmManager; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.service.WalletConnectService; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.alphawallet.app.walletconnect.WCClient; import com.alphawallet.app.walletconnect.WCSession; import com.alphawallet.app.walletconnect.entity.GetClientCallback; @@ -63,7 +68,6 @@ import io.reactivex.schedulers.Schedulers; import io.realm.Realm; import io.realm.RealmResults; -import io.realm.Sort; import timber.log.Timber; @HiltViewModel @@ -76,11 +80,12 @@ public class WalletConnectViewModel extends BaseViewModel private final KeyService keyService; private final FindDefaultNetworkInteract findDefaultNetworkInteract; private final GenericWalletInteract genericWalletInteract; + private final FetchWalletsInteract fetchWalletsInteract; private final CreateTransactionInteract createTransactionInteract; + private final WalletConnectInteract walletConnectInteract; private final RealmManager realmManager; private final GasService gasService; private final TokensService tokensService; - private final AnalyticsServiceType analyticsService; private final EthereumNetworkRepositoryType ethereumNetworkRepository; private final HashMap clientBuffer = new HashMap<>(); @@ -90,28 +95,35 @@ public class WalletConnectViewModel extends BaseViewModel @Nullable private Disposable prepareDisposable; + private final AWWalletConnectClient awWalletConnectClient; + private static final String TAG = "WCClientVM"; @Inject WalletConnectViewModel(KeyService keyService, FindDefaultNetworkInteract findDefaultNetworkInteract, - CreateTransactionInteract createTransactionInteract, + FetchWalletsInteract fetchWalletsInteract, CreateTransactionInteract createTransactionInteract, GenericWalletInteract genericWalletInteract, - RealmManager realmManager, + WalletConnectInteract walletConnectInteract, RealmManager realmManager, GasService gasService, TokensService tokensService, AnalyticsServiceType analyticsService, - EthereumNetworkRepositoryType ethereumNetworkRepository) + EthereumNetworkRepositoryType ethereumNetworkRepository, + AWWalletConnectClient awWalletConnectClient + ) { this.keyService = keyService; this.findDefaultNetworkInteract = findDefaultNetworkInteract; + this.fetchWalletsInteract = fetchWalletsInteract; this.createTransactionInteract = createTransactionInteract; this.genericWalletInteract = genericWalletInteract; + this.walletConnectInteract = walletConnectInteract; this.realmManager = realmManager; this.gasService = gasService; this.tokensService = tokensService; - this.analyticsService = analyticsService; this.ethereumNetworkRepository = ethereumNetworkRepository; + setAnalyticsService(analyticsService); + this.awWalletConnectClient = awWalletConnectClient; prepareDisposable = null; disposable = genericWalletInteract .find() @@ -198,14 +210,14 @@ public void getAuthenticationForSignature(Wallet wallet, Activity activity, Sign public void signMessage(Signable message, DAppFunction dAppFunction) { resetSignDialog(); - disposable = createTransactionInteract.sign(defaultWallet.getValue(), message, MAINNET_ID) + disposable = createTransactionInteract.sign(defaultWallet.getValue(), message) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig -> dAppFunction.DAppReturn(sig.signature, message), error -> dAppFunction.DAppError(error, message)); } - public void signTransaction(Context ctx, Web3Transaction w3tx, DAppFunction dAppFunction, String requesterURL, long chainId) + public void signTransaction(Context ctx, Web3Transaction w3tx, DAppFunction dAppFunction, String requesterURL, long chainId, Wallet fromWallet) { resetSignDialog(); EthereumMessage etm = new EthereumMessage(w3tx.getFormattedTransaction(ctx, chainId, getNetworkSymbol(chainId)).toString(), @@ -213,7 +225,7 @@ public void signTransaction(Context ctx, Web3Transaction w3tx, DAppFunction dApp if (w3tx.isConstructor()) { - disposable = createTransactionInteract.signTransaction(defaultWallet.getValue(), w3tx, chainId) + disposable = createTransactionInteract.signTransaction(fromWallet, w3tx, chainId) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig -> dAppFunction.DAppReturn(Numeric.hexStringToByteArray(sig.signature), etm), @@ -221,7 +233,7 @@ public void signTransaction(Context ctx, Web3Transaction w3tx, DAppFunction dApp } else { - disposable = createTransactionInteract.signTransaction(defaultWallet.getValue(), w3tx, chainId) + disposable = createTransactionInteract.signTransaction(fromWallet, w3tx, chainId) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig -> dAppFunction.DAppReturn(Numeric.hexStringToByteArray(sig.signature), etm), @@ -229,29 +241,47 @@ public void signTransaction(Context ctx, Web3Transaction w3tx, DAppFunction dApp } } - public void sendTransaction(final Web3Transaction finalTx, long chainId, SendTransactionInterface callback) + public void sendTransaction(final Web3Transaction finalTx, Wallet wallet, long chainId, SendTransactionInterface callback) { if (finalTx.isConstructor()) { disposable = createTransactionInteract - .createWithSig(defaultWallet.getValue(), finalTx.gasPrice, finalTx.gasLimit, finalTx.payload, chainId) + .createWithSig(wallet, finalTx.gasPrice, finalTx.gasLimit, finalTx.payload, chainId) .subscribe(txData -> callback.transactionSuccess(finalTx, txData.txHash), error -> callback.transactionError(finalTx.leafPosition, error)); } else { disposable = createTransactionInteract - .createWithSig(defaultWallet.getValue(), finalTx, chainId) + .createWithSig(wallet, finalTx, chainId) .subscribe(txData -> callback.transactionSuccess(finalTx, txData.txHash), error -> callback.transactionError(finalTx.leafPosition, error)); } } + public void sendTransaction(final Web3Transaction finalTx, long chainId, SendTransactionInterface callback) + { + sendTransaction(finalTx, defaultWallet.getValue(), chainId, callback); + } + public Single calculateGasEstimate(Wallet wallet, byte[] transactionBytes, long chainId, String sendAddress, BigDecimal sendAmount, BigInteger defaultLimit) { return gasService.calculateGasEstimate(transactionBytes, chainId, sendAddress, sendAmount.toBigInteger(), wallet, defaultLimit); } + public Single calculateGasEstimate(Wallet wallet, Web3Transaction transaction, long chainId) + { + if (transaction.isBaseTransfer()) + { + return Single.fromCallable(() -> BigInteger.valueOf(C.GAS_LIMIT_MIN)); + } + else + { + return gasService.calculateGasEstimate(org.web3j.utils.Numeric.hexStringToByteArray(transaction.payload), chainId, + transaction.recipient.toString(), transaction.value, wallet, transaction.gasLimit); + } + } + public void resetSignDialog() { keyService.resetSigningDialog(); @@ -399,19 +429,38 @@ public void updateSession(String sessionId, long sessionChainId) } } - public void deleteSession(String sessionId) + public void deleteSession(WalletConnectSessionItem session, AWWalletConnectClient.WalletConnectV2Callback callback) + { + if (session instanceof WalletConnectV2SessionItem) + { + deleteSessionV2(session, callback); + } + else + { + deleteSessionV1(session, callback); + } + } + + private void deleteSessionV2(WalletConnectSessionItem session, AWWalletConnectClient.WalletConnectV2Callback callback) + { + awWalletConnectClient.disconnect(session.sessionId, callback); + } + + private void deleteSessionV1(WalletConnectSessionItem session, AWWalletConnectClient.WalletConnectV2Callback callback) { + Timber.d("deleteSession: %s", session.sessionId); try (Realm realm = realmManager.getRealmInstance(WC_SESSION_DB)) { realm.executeTransactionAsync(r -> { RealmWCSession sessionAux = r.where(RealmWCSession.class) - .equalTo("sessionId", sessionId) + .equalTo("sessionId", session.sessionId) .findFirst(); if (sessionAux != null) { sessionAux.deleteFromRealm(); } + callback.onSessionDisconnected(); }); } } @@ -470,20 +519,7 @@ public ArrayList getSignRecords(String sessionId) public List getSessions() { - List sessions = new ArrayList<>(); - try (Realm realm = realmManager.getRealmInstance(WC_SESSION_DB)) - { - RealmResults items = realm.where(RealmWCSession.class) - .sort("lastUsageTime", Sort.DESCENDING) - .findAll(); - - for (RealmWCSession r : items) - { - sessions.add(new WalletConnectSessionItem(r)); - } - } - - return sessions; + return walletConnectInteract.getSessions(); } public void removePendingRequest(Activity activity, long id) @@ -538,6 +574,7 @@ public void onServiceConnected(ComponentName name, IBinder service) { WalletConnectService walletConnectService = ((WalletConnectService.LocalBinder) service).getService(); walletConnectService.putClient(sessionId, client); + awWalletConnectClient.updateNotification(); } @Override @@ -601,14 +638,6 @@ public String getNetworkSymbol(long chainId) return info.symbol; } - public void actionSheetConfirm(String mode) - { - AnalyticsProperties analyticsProperties = new AnalyticsProperties(); - analyticsProperties.setData("(WC)" + mode); //disambiguate signs/sends etc through WC - - analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); - } - public void prepareIfRequired() { if (prepareDisposable == null) @@ -672,10 +701,16 @@ public boolean isChainAdded(long chainId) return ethereumNetworkRepository.getNetworkByChain(chainId) != null; } - public TokensService getTokenService() { + public TokensService getTokenService() + { return tokensService; } + public Wallet findWallet(String address) + { + return fetchWalletsInteract.getWallet(address).blockingGet(); + } + public void endSession(String sessionId) { try (Realm realm = realmManager.getRealmInstance(WC_SESSION_DB)) @@ -693,4 +728,91 @@ public void endSession(String sessionId) }); } } + + // remove wallet connect sessions with no transaction history + public void removeEmptySessions(Context context, Runnable onComplete) + { + // create list of sessions which are inactive and has zero txn/sign + // delete them from realm + getInactiveSessionIds(context, sessions -> { + ArrayList sessionIdsToRemove = new ArrayList<>(); + for (String sessionId : sessions) + { + // if no txn/sign history found + if (getSignRecords(sessionId).isEmpty()) + { + // add this sessionId to the list of removable + sessionIdsToRemove.add(sessionId); + } + } + deleteSessionsFromRealm(sessionIdsToRemove, onComplete); + }); + } + + // remove all inactive wallet connect sessions + public void removeAllSessions(Context context, Runnable onSuccess) + { + getInactiveSessionIds(context, list -> { + Timber.d("removeAllSessions: sessionsToRemove: %d", list.size()); + deleteSessionsFromRealm(list, onSuccess); + }); + } + + // connects to service to check session state and gives inactive sessions + private void getInactiveSessionIds(Context context, GenericCallback> callback) + { + List sessionItems = getSessions(); // all sessions in DB + ArrayList inactiveSessions = new ArrayList<>(); + ServiceConnection connection = new ServiceConnection() + { + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + WalletConnectService walletConnectService = ((WalletConnectService.LocalBinder) service).getService(); + // loop & populate sessions which are inactive + for (WalletConnectSessionItem item : sessionItems) + { + WCClient wcClient = walletConnectService.getClient(item.sessionId); + // if client is not connected ie: session inactive + if (wcClient == null || !wcClient.isConnected()) + { + inactiveSessions.add(item.sessionId); + } + } + callback.call(inactiveSessions); // return inactive sessions to caller + } + + @Override + public void onServiceDisconnected(ComponentName name) + { + //walletConnectService = null; + Timber.tag(TAG).d("Service disconnected"); + } + }; + Intent i = new Intent(context, WalletConnectService.class); // not specifying action as no need. we just need to bind to service + context.startService(i); + context.bindService(i, connection, Service.BIND_ABOVE_CLIENT); + } + + // deletes the RealmWCSession objects with the given sessionIds present in the list + private void deleteSessionsFromRealm(List sessionIds, Runnable onSuccess) + { + Timber.d("deleteSessionsFromRealm: sessions: %s", sessionIds); + if (sessionIds.isEmpty()) + return; + try (Realm realm = realmManager.getRealmInstance(WC_SESSION_DB)) + { + realm.executeTransactionAsync(r -> { + boolean isDeleted = r.where(RealmWCSession.class) + .in("sessionId", sessionIds.toArray(new String[]{})) + .findAll() + .deleteAllFromRealm(); + Timber.d("deleteSessions: Success: %s\nList: %s", isDeleted, sessionIds); + }, onSuccess::run); + } + catch (Exception e) + { + Timber.e(e); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index 315518adfe..ff4726e11f 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -1,7 +1,6 @@ package com.alphawallet.app.viewmodel; import static com.alphawallet.app.C.EXTRA_ADDRESS; -import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; import static com.alphawallet.app.widget.CopyTextView.KEY_ADDRESS; import android.app.Activity; @@ -22,24 +21,30 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.analytics.QrScanSource; import com.alphawallet.app.entity.tokendata.TokenGroup; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; import com.alphawallet.app.interact.ChangeTokenEnableInteract; import com.alphawallet.app.interact.FetchTokensInteract; import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.repository.CoinbasePayRepository; import com.alphawallet.app.repository.OnRampRepositoryType; import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.repository.WalletItem; +import com.alphawallet.app.router.CoinbasePayRouter; import com.alphawallet.app.router.ManageWalletsRouter; import com.alphawallet.app.router.MyAddressRouter; import com.alphawallet.app.router.TokenDetailRouter; +import com.alphawallet.app.service.AnalyticsServiceType; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.RealmManager; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.ui.NameThisWalletActivity; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.TokenManagementActivity; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.alphawallet.app.widget.WalletFragmentActionsView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -48,6 +53,8 @@ import org.web3j.crypto.Keys; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; @@ -75,11 +82,13 @@ public class WalletViewModel extends BaseViewModel private final ChangeTokenEnableInteract changeTokenEnableInteract; private final PreferenceRepositoryType preferenceRepository; private final MyAddressRouter myAddressRouter; + private final CoinbasePayRouter coinbasePayRouter; private final ManageWalletsRouter manageWalletsRouter; private final RealmManager realmManager; + private final OnRampRepositoryType onRampRepository; private long lastBackupCheck = 0; private BottomSheetDialog dialog; - private final OnRampRepositoryType onRampRepository; + private final AWWalletConnectClient awWalletConnectClient; @Inject WalletViewModel( @@ -90,10 +99,13 @@ public class WalletViewModel extends BaseViewModel TokensService tokensService, ChangeTokenEnableInteract changeTokenEnableInteract, MyAddressRouter myAddressRouter, + CoinbasePayRouter coinbasePayRouter, ManageWalletsRouter manageWalletsRouter, PreferenceRepositoryType preferenceRepository, RealmManager realmManager, - OnRampRepositoryType onRampRepository) + OnRampRepositoryType onRampRepository, + AnalyticsServiceType analyticsService, + AWWalletConnectClient awWalletConnectClient) { this.fetchTokensInteract = fetchTokensInteract; this.tokenDetailRouter = tokenDetailRouter; @@ -102,25 +114,48 @@ public class WalletViewModel extends BaseViewModel this.tokensService = tokensService; this.changeTokenEnableInteract = changeTokenEnableInteract; this.myAddressRouter = myAddressRouter; + this.coinbasePayRouter = coinbasePayRouter; this.manageWalletsRouter = manageWalletsRouter; this.preferenceRepository = preferenceRepository; this.realmManager = realmManager; this.onRampRepository = onRampRepository; + this.awWalletConnectClient = awWalletConnectClient; + setAnalyticsService(analyticsService); } - public LiveData tokens() { + public LiveData tokens() + { return tokens; } - public LiveData defaultWallet() { return defaultWallet; } - public LiveData backupEvent() { return backupEvent; } - public LiveData> onFiatValues() { return fiatValues; } - public String getWalletAddr() { return defaultWallet.getValue() != null ? defaultWallet.getValue().address : ""; } - public WalletType getWalletType() { return defaultWallet.getValue() != null ? defaultWallet.getValue().type : WalletType.KEYSTORE; } + public LiveData defaultWallet() + { + return defaultWallet; + } + + public LiveData backupEvent() + { + return backupEvent; + } + + public LiveData> onFiatValues() + { + return fiatValues; + } + + public String getWalletAddr() + { + return defaultWallet.getValue() != null ? defaultWallet.getValue().address : ""; + } + + public WalletType getWalletType() + { + return defaultWallet.getValue() != null ? defaultWallet.getValue().type : WalletType.KEYSTORE; + } public void prepare() { - lastBackupCheck = System.currentTimeMillis() - BALANCE_BACKUP_CHECK_INTERVAL + 5*DateUtils.SECOND_IN_MILLIS; + lastBackupCheck = System.currentTimeMillis() - BALANCE_BACKUP_CHECK_INTERVAL + 5 * DateUtils.SECOND_IN_MILLIS; //load the activity meta list disposable = genericWalletInteract .find() @@ -129,6 +164,7 @@ public void prepare() public void reloadTokens() { + tokensService.startUpdateCycle(); if (defaultWallet.getValue() != null) { fetchTokens(defaultWallet().getValue()); @@ -210,12 +246,18 @@ public void setKeyWarningDismissTime(String walletAddr) genericWalletInteract.updateWarningTime(walletAddr); } - public void setTokenEnabled(Token token, boolean enabled) { + public void setTokenEnabled(Token token, boolean enabled) + { changeTokenEnableInteract.setEnable(defaultWallet.getValue(), token, enabled); token.tokenInfo.isEnabled = enabled; } - public void showMyAddress(Context context) + public void showBuyEthOptions(Activity activity) + { + coinbasePayRouter.buyFromSelectedChain(activity, CoinbasePayRepository.Blockchains.ETHEREUM); + } + + public void showMyAddress(Activity context) { // show bottomsheet dialog WalletFragmentActionsView actionsView = new WalletFragmentActionsView(context); @@ -223,7 +265,8 @@ public void showMyAddress(Context context) dialog.dismiss(); ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText(KEY_ADDRESS, Keys.toChecksumAddress(getWalletAddr())); - if (clipboard != null) { + if (clipboard != null) + { clipboard.setPrimaryClip(clip); } @@ -237,7 +280,7 @@ public void showMyAddress(Context context) dialog.dismiss(); Intent intent = new Intent(context, TokenManagementActivity.class); intent.putExtra(EXTRA_ADDRESS, getWalletAddr()); - context.startActivity(intent); + context.startActivityForResult(intent, C.ADDED_TOKEN_RETURN); }); actionsView.setOnRenameThisWalletClickListener(v -> { dialog.dismiss(); @@ -254,9 +297,11 @@ public void showMyAddress(Context context) dialog.show(); } - public void showQRCodeScanning(Activity activity) { - Intent intent = new Intent(activity, QRScanner.class); + public void showQRCodeScanning(Activity activity) + { + Intent intent = new Intent(activity, QRScannerActivity.class); intent.putExtra(C.EXTRA_UNIVERSAL_SCAN, true); + intent.putExtra(QrScanSource.KEY, QrScanSource.WALLET_SCREEN.getValue()); activity.startActivityForResult(intent, C.REQUEST_UNIVERSAL_SCAN); } @@ -291,12 +336,16 @@ public void showTokenDetail(Activity activity, Token token) break; case ERC721: - case ERC875_LEGACY: - case ERC875: case ERC721_LEGACY: case ERC721_TICKET: case ERC721_UNDETERMINED: - tokenDetailRouter.open(activity, token, defaultWallet.getValue(), false); //TODO: Fold this into tokenDetailRouter + case ERC721_ENUMERABLE: + tokenDetailRouter.open(activity, token, defaultWallet.getValue(), false); + break; + + case ERC875_LEGACY: + case ERC875: + tokenDetailRouter.openLegacyToken(activity, token, defaultWallet.getValue()); break; case NOT_SET: @@ -309,7 +358,8 @@ public void showTokenDetail(Activity activity, Token token) public void checkBackup(double fiatValue) { - if (TextUtils.isEmpty(getWalletAddr()) || System.currentTimeMillis() < (lastBackupCheck + BALANCE_BACKUP_CHECK_INTERVAL)) return; + if (TextUtils.isEmpty(getWalletAddr()) || System.currentTimeMillis() < (lastBackupCheck + BALANCE_BACKUP_CHECK_INTERVAL)) + return; lastBackupCheck = System.currentTimeMillis(); double walletUSDValue = tokensService.convertToUSD(fiatValue); @@ -369,22 +419,50 @@ public void showManageWallets(Context context, boolean clearStack) manageWalletsRouter.open(context, clearStack); } - public boolean isMarshMallowWarningShown() { + public boolean isMarshMallowWarningShown() + { return preferenceRepository.isMarshMallowWarningShown(); } - public void setMarshMallowWarning(boolean shown) { + public void setMarshMallowWarning(boolean shown) + { preferenceRepository.setMarshMallowWarning(shown); } public void saveAvatar(Wallet wallet) { - genericWalletInteract.updateWalletItem(wallet, WalletItem.ENS_AVATAR, () -> { }); + genericWalletInteract.updateWalletItem(wallet, WalletItem.ENS_AVATAR, () -> {}); } - public Intent getBuyIntent(String address) { + public Intent getBuyIntent(String address) + { Intent intent = new Intent(); intent.putExtra(C.DAPP_URL_LOAD, onRampRepository.getUri(address, null)); return intent; } + + public MutableLiveData> activeWalletConnectSessions() + { + return awWalletConnectClient.sessionItemMutableLiveData(); + } + + public void checkDeleteMetas(List metas) + { + List metasToDelete = new ArrayList<>(); + for (TokenCardMeta meta : metas) + { + if (meta.balance.equals("-2")) + { + metasToDelete.add(meta); + } + } + + if (metasToDelete.size() > 0) + { + disposable = tokensService.deleteTokens(metasToDelete) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java index 252967fea6..b63b939ed6 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java @@ -19,12 +19,15 @@ import com.alphawallet.app.entity.SyncCallback; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.tokendata.TokenUpdateType; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.interact.FetchWalletsInteract; import com.alphawallet.app.interact.FindDefaultNetworkInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.interact.SetDefaultWalletInteract; import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; +import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.repository.TokenRepositoryType; import com.alphawallet.app.router.HomeRouter; @@ -33,11 +36,9 @@ import com.alphawallet.app.service.KeyService; import com.alphawallet.app.service.TickerService; import com.alphawallet.app.service.TokensService; -import com.alphawallet.app.util.AWEnsResolver; +import com.alphawallet.app.util.ens.AWEnsResolver; import java.util.HashMap; -import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -56,10 +57,7 @@ @HiltViewModel public class WalletsViewModel extends BaseViewModel implements ServiceSyncCallback { - private final static String TAG = WalletsViewModel.class.getSimpleName(); - - private static final int BALANCE_CHECK_INTERVAL_SECONDS = 20; - + private static final int BALANCE_CHECK_INTERVAL_SECONDS = 30; private final SetDefaultWalletInteract setDefaultWalletInteract; private final FetchWalletsInteract fetchWalletsInteract; private final GenericWalletInteract genericWalletInteract; @@ -70,6 +68,7 @@ public class WalletsViewModel extends BaseViewModel implements ServiceSyncCallba private final TokensService tokensService; private final AWEnsResolver ensResolver; private final AssetDefinitionService assetService; + private final PreferenceRepositoryType preferenceRepository; private final EthereumNetworkRepositoryType ethereumNetworkRepository; private final TokenRepositoryType tokenRepository; @@ -80,6 +79,7 @@ public class WalletsViewModel extends BaseViewModel implements ServiceSyncCallba private final MutableLiveData createdWallet = new MutableLiveData<>(); private final MutableLiveData createWalletError = new MutableLiveData<>(); private final MutableLiveData noWalletsError = new MutableLiveData<>(); + private final MutableLiveData> baseTokens = new MutableLiveData<>(); private NetworkInfo currentNetwork; private final Map walletBalances = new HashMap<>(); @@ -113,6 +113,7 @@ public class WalletsViewModel extends BaseViewModel implements ServiceSyncCallba TokenRepositoryType tokenRepository, TickerService tickerService, AssetDefinitionService assetService, + PreferenceRepositoryType preferenceRepository, @ApplicationContext Context context) { this.setDefaultWalletInteract = setDefaultWalletInteract; @@ -126,7 +127,7 @@ public class WalletsViewModel extends BaseViewModel implements ServiceSyncCallba this.tokenRepository = tokenRepository; this.tickerService = tickerService; this.assetService = assetService; - + this.preferenceRepository = preferenceRepository; this.tokensService = new TokensService(ethereumNetworkRepository, tokenRepository, tickerService, null, null); ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(MAINNET_ID), context); @@ -153,9 +154,11 @@ public LiveData createWalletError() return createWalletError; } public LiveData noWalletsError() { return noWalletsError; } + public LiveData> baseTokens() { return baseTokens; } - public void setDefaultWallet(Wallet wallet) + public void setDefaultWallet(Wallet wallet, boolean isNewWallet) { + preferenceRepository.setNewWallet(wallet.address, isNewWallet); disposable = setDefaultWalletInteract .set(wallet) .subscribe(() -> onDefaultWallet(wallet), this::onError); @@ -174,7 +177,6 @@ private void startWalletUpdate() walletBalances.clear(); progress.postValue(true); - disposable = genericWalletInteract .find() .subscribe(this::onDefaultWallet, @@ -324,7 +326,10 @@ private void updateNextWallet() if (nextWalletToCheck != null) { Wallet w = walletUpdate.get(nextWalletToCheck); - currentWalletUpdates.put(nextWalletToCheck, startWalletSyncProcess(w)); + if (w != null) + { + currentWalletUpdates.put(nextWalletToCheck, startWalletSyncProcess(w)); + } } } @@ -365,6 +370,10 @@ public void newWallet(Activity ctx, CreateWalletCallbackInterface createCallback private void startBalanceUpdateTimer(final Wallet[] wallets) { if (balanceTimerDisposable != null && !balanceTimerDisposable.isDisposed()) balanceTimerDisposable.dispose(); + if (!tokensService.isMainNetActive()) + { + updateAllWallets(wallets, TokenUpdateType.STORED); //initially show values from database, start update 1 second later + } balanceTimerDisposable = Observable.interval(1, BALANCE_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS) //initial delay 1 second to allow view to stabilise .doOnNext(l -> getWalletsBalance(wallets)).subscribe(); @@ -377,16 +386,42 @@ private void startBalanceUpdateTimer(final Wallet[] wallets) */ private void getWalletsBalance(Wallet[] wallets) { - //loop through wallets and update balance - disposable = Observable.fromArray(wallets) - .forEach(wallet -> walletBalanceUpdate = tokensService.getChainBalance(wallet.address.toLowerCase(), currentNetwork.chainId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(newBalance -> genericWalletInteract.updateBalanceIfRequired(wallet, newBalance), e -> { })); - + if (tokensService.isMainNetActive()) + { + //loop through wallets and update balance + disposable = Observable.fromArray(wallets) + .forEach(wallet -> walletBalanceUpdate = tokensService.getChainBalance(wallet.address.toLowerCase(), currentNetwork.chainId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(newBalance -> genericWalletInteract.updateBalanceIfRequired(wallet, newBalance), e -> {})); + } + else + { + //Testnet Mode, need to update chain balances for visible chains + updateAllWallets(wallets, TokenUpdateType.ACTIVE_SYNC); + } progress.postValue(false); } + private void updateAllWallets(Wallet[] wallets, TokenUpdateType updateType) + { + disposable = Single.fromCallable(() -> { + //fetch all wallets in one go + Map walletTokenMap = new HashMap<>(); + for (Wallet wallet : wallets) + { + Token[] walletTokens = tokensService.syncChainBalances(wallet.address.toLowerCase(), updateType).blockingGet(); + if (walletTokens.length > 0) + { + walletTokenMap.put(walletTokens[0].getWallet(), walletTokens); + } + } + return walletTokenMap; + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(baseTokens::postValue, e -> {}); + } + @Override public void onCleared() { diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java b/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java new file mode 100644 index 0000000000..4b8a73e788 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java @@ -0,0 +1,422 @@ +package com.alphawallet.app.walletconnect; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static com.alphawallet.app.entity.cryptokeys.SignatureReturnType.SIGNATURE_GENERATED; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.LongSparseArray; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.C; +import com.alphawallet.app.R; +import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; +import com.alphawallet.app.entity.walletconnect.NamespaceParser; +import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; +import com.alphawallet.app.entity.walletconnect.WalletConnectV2SessionItem; +import com.alphawallet.app.interact.WalletConnectInteract; +import com.alphawallet.app.repository.KeyProvider; +import com.alphawallet.app.repository.KeyProviderFactory; +import com.alphawallet.app.service.WalletConnectV2Service; +import com.alphawallet.app.ui.HomeActivity; +import com.alphawallet.app.ui.WalletConnectV2Activity; +import com.alphawallet.app.walletconnect.util.WCMethodChecker; +import com.alphawallet.token.entity.Signable; +import com.alphawallet.token.tools.Numeric; +import com.walletconnect.android.Core; +import com.walletconnect.android.CoreClient; +import com.walletconnect.android.relay.ConnectionType; +import com.walletconnect.sign.client.Sign; +import com.walletconnect.sign.client.SignClient; +import com.walletconnect.sign.client.SignInterface; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import kotlin.Unit; +import timber.log.Timber; + +public class AWWalletConnectClient implements SignInterface.WalletDelegate +{ + private static final String TAG = AWWalletConnectClient.class.getName(); + private final WalletConnectInteract walletConnectInteract; + public static Sign.Model.SessionProposal sessionProposal; + + private final Context context; + private final MutableLiveData> sessionItemMutableLiveData = new MutableLiveData<>(Collections.emptyList()); + private final KeyProvider keyProvider = KeyProviderFactory.get(); + private final LongSparseArray requestHandlers = new LongSparseArray<>(); + private HomeActivity activity; + private boolean hasConnection; + + public AWWalletConnectClient(Context context, WalletConnectInteract walletConnectInteract) + { + this.context = context; + this.walletConnectInteract = walletConnectInteract; + hasConnection = false; + } + + public void onSessionDelete(@NonNull Sign.Model.DeletedSession deletedSession) + { + updateNotification(); + } + + public void onSessionProposal(@NonNull Sign.Model.SessionProposal sessionProposal) + { + WalletConnectV2SessionItem sessionItem = WalletConnectV2SessionItem.from(sessionProposal); + if (!validChainId(sessionItem.chains)) + { + return; + } + AWWalletConnectClient.sessionProposal = sessionProposal; + Intent intent = new Intent(context, WalletConnectV2Activity.class); + intent.putExtra("session", sessionItem); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + private boolean validChainId(List chains) + { + for (String chainId : chains) + { + try + { + Long.parseLong(chainId.split(":")[1]); + } + catch (Exception e) + { + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, String.format(context.getString(R.string.chain_not_support), chainId), Toast.LENGTH_SHORT).show()); + return false; + } + } + return true; + } + + public void onSessionRequest(@NonNull Sign.Model.SessionRequest sessionRequest) + { + String method = sessionRequest.getRequest().getMethod(); + if ("eth_signTypedData_v4".equals(method)) + { + method = "eth_signTypedData"; + } + + if (!WCMethodChecker.includes(method)) + { + reject(sessionRequest); + return; + } + + Sign.Model.Session settledSession = getSession(sessionRequest.getTopic()); + + WalletConnectV2SessionRequestHandler handler = new WalletConnectV2SessionRequestHandler(sessionRequest, settledSession, activity, this); + handler.handle(method, activity); + requestHandlers.append(sessionRequest.getRequest().getId(), handler); + } + + private Sign.Model.Session getSession(String topic) + { + List listOfSettledSessions; + + try + { + listOfSettledSessions = SignClient.INSTANCE.getListOfSettledSessions(); + } + catch (IllegalStateException e) + { + listOfSettledSessions = Collections.emptyList(); + Timber.tag(TAG).e(e); + } + + for (Sign.Model.Session session : listOfSettledSessions) + { + if (session.getTopic().equals(topic)) + { + return session; + } + } + return null; + } + + public void pair(String url, Consumer callback) + { + Core.Params.Pair pair = new Core.Params.Pair(url); + CoreClient.INSTANCE.getPairing().pair(pair, error -> { + Timber.e(error.getThrowable()); + callback.accept(error.getThrowable().getMessage()); + return null; + }); + } + + public void approve(Sign.Model.SessionRequest sessionRequest, String result) + { + Sign.Model.JsonRpcResponse jsonRpcResponse = new Sign.Model.JsonRpcResponse.JsonRpcResult(sessionRequest.getRequest().getId(), result); + Sign.Params.Response response = new Sign.Params.Response(sessionRequest.getTopic(), jsonRpcResponse); + SignClient.INSTANCE.respond(response, this::onSessionRequestApproveError); + } + + private Unit onSessionRequestApproveError(Sign.Model.Error error) + { + Timber.e(error.getThrowable()); + return null; + } + + public void reject(Sign.Model.SessionRequest sessionRequest) + { + reject(sessionRequest, context.getString(R.string.message_reject_request)); + } + + public void approve(Sign.Model.SessionProposal sessionProposal, List selectedAccounts, WalletConnectV2Callback callback) + { + String proposerPublicKey = sessionProposal.getProposerPublicKey(); + Sign.Params.Approve approve = new Sign.Params.Approve(proposerPublicKey, buildNamespaces(sessionProposal, selectedAccounts), sessionProposal.getRelayProtocol()); + SignClient.INSTANCE.approveSession(approve, this::onSessionApproveError); + callback.onSessionProposalApproved(); + new Handler().postDelayed(this::updateNotification, 3000); + } + + private Map buildNamespaces(Sign.Model.SessionProposal sessionProposal, List selectedAccounts) + { + Map result = new HashMap<>(); + Map namespaces = sessionProposal.getRequiredNamespaces(); + NamespaceParser namespaceParser = new NamespaceParser(); + namespaceParser.parseProposal(namespaces); + List accounts = toCAIP10(namespaceParser.getChains(), selectedAccounts); + for (Map.Entry entry : namespaces.entrySet()) + { + Sign.Model.Namespace.Session session = new Sign.Model.Namespace.Session(accounts, namespaceParser.getMethods(), namespaceParser.getEvents(), null); + result.put(entry.getKey(), session); + } + return result; + } + + private List toCAIP10(List chains, List selectedAccounts) + { + List result = new ArrayList<>(); + for (String chain : chains) + { + for (String account : selectedAccounts) + { + result.add(chain + ":" + account); + } + } + return result; + } + + private Unit onSessionApproveError(Sign.Model.Error error) + { + Timber.e(error.getThrowable()); + Toast.makeText(context, error.getThrowable().getLocalizedMessage(), Toast.LENGTH_SHORT).show(); + return null; + } + + public MutableLiveData> sessionItemMutableLiveData() + { + return sessionItemMutableLiveData; + } + + public void updateNotification() + { + walletConnectInteract.fetchSessions(context, items -> { + sessionItemMutableLiveData.postValue(items); + updateService(context, items); + }); + } + + private void updateService(Context context, List walletConnectSessionItems) + { + if (walletConnectSessionItems.isEmpty()) + { + context.stopService(new Intent(context, WalletConnectV2Service.class)); + } + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + Intent service = new Intent(context, WalletConnectV2Service.class); + context.startForegroundService(service); + } + } + + + public void reject(Sign.Model.SessionProposal sessionProposal, WalletConnectV2Callback callback) + { + + SignClient.INSTANCE.rejectSession( + new Sign.Params.Reject(sessionProposal.getProposerPublicKey(), context.getString(R.string.message_reject_request)), + this::onSessionRejectError); + callback.onSessionProposalRejected(); + } + + private Unit onSessionRejectError(Sign.Model.Error error) + { + Timber.e(error.getThrowable()); + return null; + } + + public void disconnect(String sessionId, WalletConnectV2Callback callback) + { + SignClient.INSTANCE.disconnect(new Sign.Params.Disconnect(sessionId), this::onDisconnectError); + callback.onSessionDisconnected(); + updateNotification(); + } + + private Unit onDisconnectError(Sign.Model.Error error) + { + Timber.e(error.getThrowable()); + return null; + } + + public void reject(Sign.Model.SessionRequest sessionRequest, String failMessage) + { + Sign.Model.JsonRpcResponse jsonRpcResponse = new Sign.Model.JsonRpcResponse.JsonRpcError(sessionRequest.getRequest().getId(), 0, failMessage); + Sign.Params.Response response = new Sign.Params.Response(sessionRequest.getTopic(), jsonRpcResponse); + SignClient.INSTANCE.respond(response, this::onSessionRequestRejectError); + } + + private Unit onSessionRequestRejectError(Sign.Model.Error error) + { + Timber.e(error.getThrowable()); + return null; + } + + public void init(HomeActivity homeActivity) + { + activity = homeActivity; + } + + public void init(Application application) + { + Core.Model.AppMetaData appMetaData = getAppMetaData(application); + String relayServer = String.format("%s/?projectId=%s", C.WALLET_CONNECT_REACT_APP_RELAY_URL, keyProvider.getWalletConnectProjectId()); + CoreClient coreClient = CoreClient.INSTANCE; + coreClient.initialize(appMetaData, relayServer, ConnectionType.AUTOMATIC, application, null); + + SignClient.INSTANCE.initialize(new Sign.Params.Init(coreClient), e -> + { + Timber.tag(TAG).e("Init failed: %s", e.getThrowable().getMessage()); + return null; + }); + + try + { + SignClient.INSTANCE.setWalletDelegate(this); + } + catch (Exception e) + { + Timber.tag(TAG).e(e); + } + } + + @NonNull + private Core.Model.AppMetaData getAppMetaData(Application application) + { + String name = application.getString(R.string.app_name); + String url = C.ALPHAWALLET_WEBSITE; + String[] icons = {C.ALPHA_WALLET_LOGO_URL}; + String description = "The ultimate Web3 Wallet to power your tokens."; + String redirect = "kotlin-responder-wc:/request"; + return new Core.Model.AppMetaData(name, description, url, Arrays.asList(icons), redirect); + } + + public void shutdown() + { + Timber.tag(TAG).i("shutdown"); + } + + public void onConnectionStateChange(@NonNull Sign.Model.ConnectionState connectionState) + { + Timber.tag(TAG).i("onConnectionStateChange"); + hasConnection = connectionState.isAvailable(); + } + + public void onSessionSettleResponse(@NonNull Sign.Model.SettledSessionResponse settledSessionResponse) + { + Timber.tag(TAG).i("onSessionSettleResponse"); + } + + public void onSessionUpdateResponse(@NonNull Sign.Model.SessionUpdateResponse sessionUpdateResponse) + { + Timber.tag(TAG).i("onSessionUpdateResponse"); + } + + public void onError(Sign.Model.Error error) + { + Timber.e(error.getThrowable()); + } + + public void signComplete(SignatureFromKey signatureFromKey, Signable signable) + { + if (hasConnection) + { + onSign(signatureFromKey, getHandler(signable.getCallbackId())); //have valid connection, can send response + } + else + { + new Handler().postDelayed(() -> signComplete(signatureFromKey, signable), 1000); //Delay by 1 second and check again + } + } + + public void signFail(String error, Signable signable) + { + final WalletConnectV2SessionRequestHandler requestHandler = getHandler(signable.getCallbackId()); + + Timber.i("sign fail: %s", error); + reject(requestHandler.getSessionRequest(), error); + } + + //Sign Dialog (and later tx dialog) was dismissed + public void dismissed(long callbackId) + { + final WalletConnectV2SessionRequestHandler requestHandler = getHandler(callbackId); + if (requestHandler != null) + { + reject(requestHandler.getSessionRequest(), activity.getString(R.string.message_reject_request)); + } + } + + private WalletConnectV2SessionRequestHandler getHandler(long callbackId) + { + WalletConnectV2SessionRequestHandler handler = requestHandlers.get(callbackId); + requestHandlers.remove(callbackId); + return handler; + } + + private void onSign(SignatureFromKey signatureFromKey, WalletConnectV2SessionRequestHandler requestHandler) + { + if (signatureFromKey.sigType == SIGNATURE_GENERATED) + { + String result = Numeric.toHexString(signatureFromKey.signature); + approve(requestHandler.getSessionRequest(), result); + } + else + { + Timber.i("sign fail: %s", signatureFromKey.failMessage); + reject(requestHandler.getSessionRequest(), signatureFromKey.failMessage); + } + } + + public interface WalletConnectV2Callback + { + default void onSessionProposalApproved() + { + } + + default void onSessionProposalRejected() + { + } + + default void onSessionDisconnected() + { + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java b/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java new file mode 100644 index 0000000000..f159970199 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java @@ -0,0 +1,199 @@ +package com.alphawallet.app.walletconnect; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; + +import com.alphawallet.app.entity.DAppFunction; +import com.alphawallet.app.entity.SendTransactionInterface; +import com.alphawallet.app.entity.SignAuthenticationCallback; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; +import com.alphawallet.app.viewmodel.WalletConnectViewModel; +import com.alphawallet.app.walletconnect.entity.WCEthereumTransaction; +import com.alphawallet.app.walletconnect.util.WalletConnectHelper; +import com.alphawallet.app.web3.entity.Web3Transaction; +import com.alphawallet.app.widget.ActionSheetDialog; +import com.alphawallet.token.entity.Signable; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.walletconnect.sign.client.Sign; + +import org.web3j.utils.Numeric; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class TransactionDialogBuilder extends DialogFragment +{ + private final Activity activity; + private final Sign.Model.SessionRequest sessionRequest; + private final Sign.Model.Session settledSession; + private final AWWalletConnectClient awWalletConnectClient; + private final boolean signOnly; + private WalletConnectViewModel viewModel; + private ActionSheetDialog actionSheetDialog; + private final ActivityResultLauncher activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> actionSheetDialog.setCurrentGasIndex(result)); + + public TransactionDialogBuilder(Activity activity, Sign.Model.SessionRequest sessionRequest, Sign.Model.Session settledSession, AWWalletConnectClient awWalletConnectClient, boolean signOnly) + { + this.activity = activity; + this.sessionRequest = sessionRequest; + this.settledSession = settledSession; + this.awWalletConnectClient = awWalletConnectClient; + this.signOnly = signOnly; + + initViewModel(); + } + + private void initViewModel() + { + viewModel = new ViewModelProvider((ViewModelStoreOwner) activity) + .get(WalletConnectViewModel.class); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) + { + Type listType = new TypeToken>() + { + }.getType(); + List list = new Gson().fromJson(sessionRequest.getRequest().getParams(), listType); + WCEthereumTransaction wcTx = list.get(0); + final Web3Transaction w3Tx = new Web3Transaction(wcTx, 0); + Wallet fromWallet = viewModel.findWallet(wcTx.getFrom()); + Token token = viewModel.getTokensService().getTokenOrBase(WalletConnectHelper.getChainId(Objects.requireNonNull(sessionRequest.getChainId())), w3Tx.recipient.toString()); + actionSheetDialog = new ActionSheetDialog(activity, w3Tx, token, "", w3Tx.recipient.toString(), viewModel.getTokensService(), new ActionSheetCallback() + { + @Override + public void getAuthorisation(SignAuthenticationCallback callback) + { + viewModel.getAuthenticationForSignature(fromWallet, activity, callback); + } + + @Override + public void signTransaction(Web3Transaction tx) + { + signMessage(fromWallet, tx, awWalletConnectClient); + } + + @Override + public void sendTransaction(Web3Transaction tx) + { + TransactionDialogBuilder.this.sendTransaction(fromWallet, tx, awWalletConnectClient); + } + + @Override + public void dismissed(String txHash, long callbackId, boolean actionCompleted) + { + if (!actionCompleted) + { + awWalletConnectClient.reject(sessionRequest); + } + } + + @Override + public void notifyConfirm(String mode) + { + } + + @Override + public ActivityResultLauncher gasSelectLauncher() + { + return activityResultLauncher; + } + }); + actionSheetDialog.setSigningWallet(fromWallet.address); + if (signOnly) + { + actionSheetDialog.setSignOnly(); + } + String url = Objects.requireNonNull(settledSession.getMetaData()).getUrl(); + actionSheetDialog.setURL(url); + actionSheetDialog.setCanceledOnTouchOutside(false); + actionSheetDialog.waitForEstimate(); + + viewModel.calculateGasEstimate(fromWallet, Numeric.hexStringToByteArray(w3Tx.payload), + WalletConnectHelper.getChainId(Objects.requireNonNull(sessionRequest.getChainId())), w3Tx.recipient.toString(), new BigDecimal(w3Tx.value), w3Tx.gasLimit) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(actionSheetDialog::setGasEstimate, + Throwable::printStackTrace) + .isDisposed(); + + return actionSheetDialog; + } + + private void signMessage(Wallet fromWallet, Web3Transaction tx, AWWalletConnectClient awWalletConnectClient) + { + viewModel.signTransaction(actionSheetDialog.getContext(), tx, new DAppFunction() + { + @Override + public void DAppError(Throwable error, Signable message) + { + reject(error, awWalletConnectClient); + } + + @Override + public void DAppReturn(byte[] data, Signable message) + { + approve(Numeric.toHexString(data), awWalletConnectClient); + actionSheetDialog.transactionWritten("."); + } + }, Objects.requireNonNull(settledSession.getMetaData()).getUrl(), WalletConnectHelper.getChainId(Objects.requireNonNull(sessionRequest.getChainId())), fromWallet); + } + + private void sendTransaction(Wallet wallet, Web3Transaction tx, AWWalletConnectClient awWalletConnectClient) + { + viewModel.sendTransaction(tx, wallet, WalletConnectHelper.getChainId(Objects.requireNonNull(sessionRequest.getChainId())), new SendTransactionInterface() + { + @Override + public void transactionSuccess(Web3Transaction web3Tx, String hashData) + { + approve(hashData, awWalletConnectClient); + actionSheetDialog.transactionWritten(hashData); + } + + @Override + public void transactionError(long callbackId, Throwable error) + { + reject(error, awWalletConnectClient); + } + }); + } + + private void reject(Throwable error, AWWalletConnectClient awWalletConnectClient) + { + Toast.makeText(activity, error.getMessage(), Toast.LENGTH_SHORT).show(); + awWalletConnectClient.reject(sessionRequest); + actionSheetDialog.dismiss(); + } + + private void approve(String hashData, AWWalletConnectClient awWalletConnectClient) + { + new Handler(Looper.getMainLooper()).postDelayed(() -> + awWalletConnectClient.approve(sessionRequest, hashData), 5000); + } + +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/WCClient.kt b/app/src/main/java/com/alphawallet/app/walletconnect/WCClient.kt index 0eb706ad90..53ac1bfd10 100644 --- a/app/src/main/java/com/alphawallet/app/walletconnect/WCClient.kt +++ b/app/src/main/java/com/alphawallet/app/walletconnect/WCClient.kt @@ -408,6 +408,7 @@ open class WCClient : WebSocketListener() { WCMethod.ADD_ETHEREUM_CHAIN -> { handleAddChain(request) } + else -> {} } } diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/WalletConnectV2SessionRequestHandler.java b/app/src/main/java/com/alphawallet/app/walletconnect/WalletConnectV2SessionRequestHandler.java new file mode 100644 index 0000000000..bbd0613211 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/WalletConnectV2SessionRequestHandler.java @@ -0,0 +1,78 @@ +package com.alphawallet.app.walletconnect; + +import android.app.Activity; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; + +import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; +import com.alphawallet.app.walletconnect.entity.BaseRequest; +import com.alphawallet.app.walletconnect.entity.EthSignRequest; +import com.alphawallet.app.widget.ActionSheet; +import com.alphawallet.app.widget.ActionSheetSignDialog; +import com.alphawallet.token.entity.Signable; +import com.walletconnect.sign.client.Sign; + +import java.util.List; +import java.util.Objects; + +import timber.log.Timber; + +public class WalletConnectV2SessionRequestHandler +{ + private final Sign.Model.SessionRequest sessionRequest; + private final Sign.Model.Session settledSession; + private final Activity activity; + private final AWWalletConnectClient client; + + public WalletConnectV2SessionRequestHandler(Sign.Model.SessionRequest sessionRequest, Sign.Model.Session settledSession, Activity activity, AWWalletConnectClient client) + { + this.sessionRequest = sessionRequest; + this.settledSession = settledSession; + this.activity = activity; + this.client = client; + } + + public void handle(String method, ActionSheetCallback aCallback) + { + activity.runOnUiThread(() -> { + showDialog(method, aCallback); + }); + } + + public Sign.Model.SessionRequest getSessionRequest() + { + return sessionRequest; + } + + private void showDialog(String method, ActionSheetCallback aCallback) + { + boolean isSignTransaction = "eth_signTransaction".equals(method); + boolean isSendTransaction = "eth_sendTransaction".equals(method); + if (isSendTransaction || isSignTransaction) + { + TransactionDialogBuilder transactionDialogBuilder = new TransactionDialogBuilder(activity, sessionRequest, settledSession, client, isSignTransaction); + FragmentManager fragmentManager = ((AppCompatActivity) activity).getSupportFragmentManager(); + transactionDialogBuilder.show(fragmentManager, "wc_call"); + return; + } + + BaseRequest signRequest = EthSignRequest.getSignRequest(sessionRequest); + if (signRequest != null) + { + Signable signable = signRequest.getSignable(sessionRequest.getRequest().getId(), settledSession.getMetaData().getUrl()); + ActionSheet actionSheet = new ActionSheetSignDialog(activity, aCallback, signable); + actionSheet.setSigningWallet(signRequest.getWalletAddress()); + List icons = Objects.requireNonNull(settledSession.getMetaData()).getIcons(); + if (!icons.isEmpty()) + { + actionSheet.setIcon(icons.get(0)); + } + actionSheet.show(); + } + else + { + Timber.e("Method %s not supported.", method); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/entity/BaseRequest.java b/app/src/main/java/com/alphawallet/app/walletconnect/entity/BaseRequest.java new file mode 100644 index 0000000000..a4acdb9142 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/entity/BaseRequest.java @@ -0,0 +1,59 @@ +package com.alphawallet.app.walletconnect.entity; + +import static java.util.Arrays.asList; + +import com.alphawallet.token.entity.Signable; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.util.List; + +import timber.log.Timber; + +public abstract class BaseRequest +{ + private static final String TAG = BaseRequest.class.getName(); + protected String rawParams; + private final WCEthereumSignMessage.WCSignType type; + protected List params; + + public BaseRequest(String rawParams, WCEthereumSignMessage.WCSignType type) + { + Timber.tag(TAG).i(rawParams); + + this.rawParams = rawParams; + this.type = type; + try + { + params = new Gson().fromJson(rawParams, new TypeToken>() + { + }.getType()); + } + catch (Exception e) + { + String unwrapped = unwrap(rawParams); + int index = unwrapped.indexOf(","); + params = asList(unwrap(unwrapped.substring(0, index)), unwrapped.substring(index + 1)); + } + } + + protected String unwrap(String src) + { + StringBuilder stringBuilder = new StringBuilder(src); + return stringBuilder.substring(1, stringBuilder.length() - 1); + } + + protected String getMessage() + { + return new WCEthereumSignMessage(params, type).getData(); + } + + public abstract Signable getSignable(); + + public Signable getSignable(long callbackId, String origin) + { + return null; + } + + public abstract String getWalletAddress(); +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/entity/EthSignRequest.java b/app/src/main/java/com/alphawallet/app/walletconnect/entity/EthSignRequest.java new file mode 100644 index 0000000000..c011268883 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/entity/EthSignRequest.java @@ -0,0 +1,34 @@ +package com.alphawallet.app.walletconnect.entity; + +import com.walletconnect.sign.client.Sign; + +/** + * Created by JB on 21/11/2022. + */ +public abstract class EthSignRequest +{ + public static BaseRequest getSignRequest(Sign.Model.SessionRequest sessionRequest) + { + BaseRequest signRequest = null; + + switch (sessionRequest.getRequest().getMethod()) + { + case "eth_sign": + // see https://docs.walletconnect.org/json-rpc-api-methods/ethereum + // WalletConnect shouldn't provide access to deprecated eth_sign, as it can be used to scam people + signRequest = new SignRequest(sessionRequest.getRequest().getParams()); + break; + case "personal_sign": + signRequest = new SignPersonalMessageRequest(sessionRequest.getRequest().getParams()); + break; + case "eth_signTypedData": + case "eth_signTypedData_v4": + signRequest = new SignTypedDataRequest(sessionRequest.getRequest().getParams()); + break; + default: + break; + } + + return signRequest; + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignPersonalMessageRequest.java b/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignPersonalMessageRequest.java new file mode 100644 index 0000000000..e70f1af78e --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignPersonalMessageRequest.java @@ -0,0 +1,31 @@ +package com.alphawallet.app.walletconnect.entity; + +import com.alphawallet.token.entity.EthereumMessage; +import com.alphawallet.token.entity.SignMessageType; +import com.alphawallet.token.entity.Signable; + +public class SignPersonalMessageRequest extends BaseRequest +{ + public SignPersonalMessageRequest(String params) + { + super(params, WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE); + } + + @Override + public Signable getSignable() + { + return new EthereumMessage(getMessage(), "", 0, SignMessageType.SIGN_PERSONAL_MESSAGE); + } + + @Override + public Signable getSignable(long callbackId, String origin) + { + return new EthereumMessage(getMessage(), origin, callbackId, SignMessageType.SIGN_PERSONAL_MESSAGE); + } + + @Override + public String getWalletAddress() + { + return params.get(1); + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignRequest.java b/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignRequest.java new file mode 100644 index 0000000000..35edc28261 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignRequest.java @@ -0,0 +1,31 @@ +package com.alphawallet.app.walletconnect.entity; + +import com.alphawallet.token.entity.EthereumMessage; +import com.alphawallet.token.entity.SignMessageType; +import com.alphawallet.token.entity.Signable; + +public class SignRequest extends BaseRequest +{ + public SignRequest(String params) + { + super(params, WCEthereumSignMessage.WCSignType.MESSAGE); + } + + @Override + public Signable getSignable() + { + return new EthereumMessage(getMessage(), "", 0, SignMessageType.SIGN_MESSAGE); + } + + @Override + public Signable getSignable(long callbackId, String origin) + { + return new EthereumMessage(getMessage(), origin, callbackId, SignMessageType.SIGN_MESSAGE); + } + + @Override + public String getWalletAddress() + { + return params.get(0); + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignTypedDataRequest.java b/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignTypedDataRequest.java new file mode 100644 index 0000000000..215a3ba074 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/entity/SignTypedDataRequest.java @@ -0,0 +1,29 @@ +package com.alphawallet.app.walletconnect.entity; + +import com.alphawallet.app.entity.CryptoFunctions; +import com.alphawallet.token.entity.EthereumTypedMessage; +import com.alphawallet.token.entity.Signable; + +public class SignTypedDataRequest extends BaseRequest +{ + public SignTypedDataRequest(String params) + { + super(params, WCEthereumSignMessage.WCSignType.TYPED_MESSAGE); + } + + public String getWalletAddress() + { + return params.get(0); + } + + public Signable getSignable() + { + return new EthereumTypedMessage(getMessage(), "", 0, new CryptoFunctions()); + } + + @Override + public Signable getSignable(long callbackId, String origin) + { + return new EthereumTypedMessage(getMessage(), origin, callbackId, new CryptoFunctions()); + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/util/WCMethodChecker.java b/app/src/main/java/com/alphawallet/app/walletconnect/util/WCMethodChecker.java new file mode 100644 index 0000000000..329e8ef504 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/util/WCMethodChecker.java @@ -0,0 +1,28 @@ +package com.alphawallet.app.walletconnect.util; + +import com.alphawallet.app.walletconnect.entity.WCMethod; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.List; + +public class WCMethodChecker +{ + private static final List methods; + + static + { + Gson gson = new Gson(); + String json = gson.toJson(WCMethod.values()); + Type type = new TypeToken>() + { + }.getType(); + methods = gson.fromJson(json, type); + } + + public static boolean includes(String method) + { + return methods.contains(method); + } +} diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/util/WalletConnectHelper.java b/app/src/main/java/com/alphawallet/app/walletconnect/util/WalletConnectHelper.java new file mode 100644 index 0000000000..61e2aac849 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/walletconnect/util/WalletConnectHelper.java @@ -0,0 +1,14 @@ +package com.alphawallet.app.walletconnect.util; + +public abstract class WalletConnectHelper +{ + public static boolean isWalletConnectV1(String text) + { + return text.contains("@1"); + } + + public static long getChainId(String chainId) + { + return Long.parseLong(chainId.split(":")[1]); + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3/SignCallbackJSInterface.java b/app/src/main/java/com/alphawallet/app/web3/SignCallbackJSInterface.java index 37ad9670f0..47d34b7bda 100644 --- a/app/src/main/java/com/alphawallet/app/web3/SignCallbackJSInterface.java +++ b/app/src/main/java/com/alphawallet/app/web3/SignCallbackJSInterface.java @@ -77,7 +77,7 @@ public void signTransaction( String gasLimit, String gasPrice, String payload) { - if (value.equals("undefined") || value == null) value = "0"; + if (value == null || value.equals("undefined")) value = "0"; if (gasPrice == null) gasPrice = "0"; Web3Transaction transaction = new Web3Transaction( TextUtils.isEmpty(recipient) ? Address.EMPTY : new Address(recipient), diff --git a/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java b/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java index f48d1f2192..3cd7d3663f 100644 --- a/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java +++ b/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java @@ -2,21 +2,16 @@ import static androidx.webkit.WebSettingsCompat.FORCE_DARK_OFF; import static androidx.webkit.WebSettingsCompat.FORCE_DARK_ON; +import static com.alphawallet.app.service.AssetDefinitionService.ASSET_DETAIL_VIEW_NAME; +import static com.alphawallet.app.service.AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME; +import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_ERROR; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; -import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.webkit.WebSettingsCompat; -import androidx.webkit.WebViewFeature; - import android.text.TextUtils; -import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Base64; -import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.webkit.ConsoleMessage; @@ -30,12 +25,16 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewFeature; + import com.alphawallet.app.BuildConfig; import com.alphawallet.app.R; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokenscript.TokenScriptRenderCallback; import com.alphawallet.app.entity.tokenscript.WebCompletionCallback; -import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.entity.RealmAuxData; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.util.Utils; @@ -47,11 +46,11 @@ import com.alphawallet.token.entity.Signable; import com.alphawallet.token.entity.TicketRange; import com.alphawallet.token.entity.TokenScriptResult; +import com.alphawallet.token.entity.ViewType; import com.alphawallet.token.tools.TokenDefinition; import org.jetbrains.annotations.NotNull; -import java.io.IOException; import java.io.LineNumberReader; import java.io.StringReader; import java.math.BigInteger; @@ -64,10 +63,6 @@ import io.realm.RealmResults; import timber.log.Timber; -import static com.alphawallet.app.service.AssetDefinitionService.ASSET_DETAIL_VIEW_NAME; -import static com.alphawallet.app.service.AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME; -import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_ERROR; - /** * Created by James on 3/04/2019. * Stormbird in Singapore @@ -239,8 +234,9 @@ public void setChainId(long chainId) { jsInjectorClient.setChainId(chainId); } - public void setRpcUrl(@NonNull long chainId) { - jsInjectorClient.setRpcUrl(EthereumNetworkRepository.getDefaultNodeURL(chainId)); + public void setRpcUrl(@NonNull String useRPC) + { + jsInjectorClient.setRpcUrl(useRPC); } public void onSignPersonalMessageSuccessful(@NotNull Signable message, String signHex) { @@ -342,9 +338,9 @@ public String injectStyleAndWrapper(String viewData, String style) return jsInjectorClient.injectStyleAndWrap(viewData, style); } - public void setLayout(Token token, boolean iconified) + public void setLayout(Token token, ViewType iconified) { - if (iconified && token.itemViewHeight > 0) + if (iconified == ViewType.ITEM_VIEW && token.itemViewHeight > 0) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, token.itemViewHeight); setLayoutParams(params); @@ -408,7 +404,7 @@ public void onReceivedError(WebView view, WebResourceRequest request, WebResourc // Rendering public void displayTicketHolder(Token token, TicketRange range, AssetDefinitionService assetService) { - displayTicketHolder(token, range, assetService, true); + displayTicketHolder(token, range, assetService, ViewType.ITEM_VIEW); } /** @@ -417,7 +413,7 @@ public void displayTicketHolder(Token token, TicketRange range, AssetDefinitionS * @param range * @param assetService */ - public void displayTicketHolder(Token token, TicketRange range, AssetDefinitionService assetService, boolean iconified) + public void displayTicketHolder(Token token, TicketRange range, AssetDefinitionService assetService, ViewType iconified) { //need to wait until the assetDefinitionService has finished loading assets assetService.getAssetDefinitionASync(token.tokenInfo.chainId, token.tokenInfo.address) @@ -431,7 +427,7 @@ private void loadingError(Throwable e) Timber.e(e); } - private void renderTicketHolder(Token token, TokenDefinition td, TicketRange range, AssetDefinitionService assetService, boolean iconified) + private void renderTicketHolder(Token token, TokenDefinition td, TicketRange range, AssetDefinitionService assetService, ViewType iconified) { if (td != null && td.holdingToken != null) { @@ -460,7 +456,7 @@ private void showLegacyView(Token token, TicketRange range) loadData(displayData, "text/html", "utf-8"); } - public void renderTokenscriptView(Token token, TicketRange range, AssetDefinitionService assetService, boolean itemView) + public void renderTokenscriptView(Token token, TicketRange range, AssetDefinitionService assetService, ViewType itemView) { BigInteger tokenId = range.tokenIds.get(0); @@ -482,10 +478,20 @@ public void renderTokenscriptView(Token token, TicketRange range, AssetDefinitio * @param iconified * @param range */ - private void displayTicket(Token token, AssetDefinitionService assetService, StringBuilder attrs, boolean iconified, TicketRange range) + private void displayTicket(Token token, AssetDefinitionService assetService, StringBuilder attrs, ViewType iconified, TicketRange range) { setVisibility(View.VISIBLE); - String viewName = iconified ? ASSET_SUMMARY_VIEW_NAME : ASSET_DETAIL_VIEW_NAME; + String viewName; + switch (iconified) + { + case VIEW: + default: + viewName = ASSET_DETAIL_VIEW_NAME; + break; + case ITEM_VIEW: + viewName = ASSET_SUMMARY_VIEW_NAME; + break; + } String view = assetService.getTokenView(token.tokenInfo.chainId, token.getAddress(), viewName); if (TextUtils.isEmpty(view)) view = buildViewError(token, range, viewName); diff --git a/app/src/main/java/com/alphawallet/app/web3/entity/FunctionCallback.java b/app/src/main/java/com/alphawallet/app/web3/entity/FunctionCallback.java index 203a8853c0..97e5ad4ebb 100644 --- a/app/src/main/java/com/alphawallet/app/web3/entity/FunctionCallback.java +++ b/app/src/main/java/com/alphawallet/app/web3/entity/FunctionCallback.java @@ -1,15 +1,11 @@ package com.alphawallet.app.web3.entity; -import com.alphawallet.app.entity.DAppFunction; -import com.alphawallet.token.entity.Signable; - /** * Created by James on 6/04/2019. * Stormbird in Singapore */ public interface FunctionCallback { - void signMessage(Signable sign, DAppFunction dAppFunction); void functionSuccess(); void functionFailed(); } diff --git a/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java b/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java index d7afc9b159..c65b9cb328 100644 --- a/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java +++ b/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java @@ -12,7 +12,7 @@ import com.alphawallet.app.util.Hex; import com.alphawallet.app.util.StyledStringBuilder; import com.alphawallet.app.walletconnect.entity.WCEthereumTransaction; -import com.alphawallet.app.widget.ActionSheetMode; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.token.entity.MagicLinkInfo; import org.web3j.protocol.core.methods.request.Transaction; @@ -234,6 +234,11 @@ public boolean isConstructor() return (recipient.equals(Address.EMPTY) && payload != null); } + public boolean isBaseTransfer() + { + return payload == null || payload.equals("0x"); + } + /** * Can be used anywhere to generate an 'instant' human readable transaction dump * @param ctx @@ -245,35 +250,35 @@ public boolean isConstructor() public CharSequence getFormattedTransaction(Context ctx, long chainId, String symbol) { StyledStringBuilder sb = new StyledStringBuilder(); - sb.startStyleGroup().append(ctx.getString(R.string.to)).append(":\n "); + sb.startStyleGroup().append(ctx.getString(R.string.recipient)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); - sb.append(recipient.toString()); + sb.append(recipient.toString()).append("\n"); - sb.startStyleGroup().append("\n").append(ctx.getString(R.string.value)).append(":\n "); + sb.startStyleGroup().append("\n").append(ctx.getString(R.string.value)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); sb.append(BalanceUtils.getScaledValueWithLimit(new BigDecimal(value), 18)); - sb.append(" ").append(symbol); + sb.append(" ").append(symbol).append("\n"); - sb.startStyleGroup().append("\n").append(ctx.getString(R.string.label_gas_price)).append(":\n "); + sb.startStyleGroup().append("\n").append(ctx.getString(R.string.label_gas_price)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); - sb.append(BalanceUtils.weiToGwei(gasPrice)); + sb.append(BalanceUtils.weiToGwei(gasPrice)).append("\n"); - sb.startStyleGroup().append("\n").append(ctx.getString(R.string.label_gas_limit)).append(":\n "); + sb.startStyleGroup().append("\n").append(ctx.getString(R.string.label_gas_limit)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); - sb.append(gasLimit.toString()); + sb.append(gasLimit.toString()).append("\n"); - sb.startStyleGroup().append("\n").append(ctx.getString(R.string.label_nonce)).append(":\n "); + sb.startStyleGroup().append("\n").append(ctx.getString(R.string.label_nonce)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); - sb.append(String.valueOf(nonce)); + sb.append(String.valueOf(nonce)).append("\n"); if (!TextUtils.isEmpty(payload)) { - sb.startStyleGroup().append("\n").append(ctx.getString(R.string.payload)).append(":\n "); + sb.startStyleGroup().append("\n").append(ctx.getString(R.string.payload)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); - sb.append(payload); + sb.append(payload).append("\n"); } - sb.startStyleGroup().append("\n").append(ctx.getString(R.string.subtitle_network)).append(":\n "); + sb.startStyleGroup().append("\n").append(ctx.getString(R.string.subtitle_network)).append(": \n"); sb.setStyle(new StyleSpan(Typeface.BOLD)); sb.append(MagicLinkInfo.getNetworkNameById(chainId)); diff --git a/app/src/main/java/com/alphawallet/app/web3j/StructuredDataEncoder.java b/app/src/main/java/com/alphawallet/app/web3j/StructuredDataEncoder.java index 1ea36ddbd5..7113f89ade 100644 --- a/app/src/main/java/com/alphawallet/app/web3j/StructuredDataEncoder.java +++ b/app/src/main/java/com/alphawallet/app/web3j/StructuredDataEncoder.java @@ -13,6 +13,7 @@ package com.alphawallet.app.web3j; import com.fasterxml.jackson.databind.ObjectMapper; +import static org.web3j.abi.datatypes.Type.MAX_BYTE_LENGTH; import org.web3j.abi.TypeEncoder; import org.web3j.abi.datatypes.AbiTypes; @@ -26,6 +27,7 @@ import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -37,6 +39,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static org.web3j.abi.datatypes.Type.MAX_BYTE_LENGTH; import static org.web3j.crypto.Hash.sha3; import static org.web3j.crypto.Hash.sha3String; @@ -93,14 +96,20 @@ public Set getDependencies(String primaryType) { remainingTypes.remove(remainingTypes.size() - 1); deps.add(structName); - for (StructuredData.Entry entry : types.get(primaryType)) { - if (!types.containsKey(entry.getType())) { + for (StructuredData.Entry entry : types.get(structName)) { + String declarationFieldTypeName = entry.getType(); + String baseDeclarationTypeName = + arrayTypePattern.matcher(declarationFieldTypeName).find() + ? declarationFieldTypeName.substring( + 0, declarationFieldTypeName.indexOf('[')) + : declarationFieldTypeName; + if (!types.containsKey(baseDeclarationTypeName)) { // Don't expand on non-user defined types - } else if (deps.contains(entry.getType())) { + } else if (deps.contains(baseDeclarationTypeName)) { // Skip types which are already expanded } else { // Encountered a user defined type - remainingTypes.add(entry.getType()); + remainingTypes.add(baseDeclarationTypeName); } } } @@ -110,6 +119,14 @@ public Set getDependencies(String primaryType) { public String encodeStruct(String structName) { HashMap> types = jsonMessageObject.getTypes(); + if (!types.containsKey("EIP712Domain")) + { + types.put("EIP712Domain", Arrays.asList( + new StructuredData.Entry("name", "string"), + new StructuredData.Entry("version", "string"), + new StructuredData.Entry("verifyingContract", "address") + )); + } StringBuilder structRepresentation = new StringBuilder(structName + "("); for (StructuredData.Entry entry : types.get(structName)) { @@ -233,7 +250,23 @@ private byte[] convertToEncodedItem(String baseType, Object data) { try { if (baseType.toLowerCase().startsWith("uint") || baseType.toLowerCase().startsWith("int")) { - hashBytes = convertToBigInt(data).toByteArray(); + BigInteger value = convertToBigInt(data); + if (value.signum() >= 0) { + hashBytes = Numeric.toBytesPadded(convertToBigInt(data), MAX_BYTE_LENGTH); + } else { + byte signPadding = (byte) 0xff; + byte[] rawValue = convertToBigInt(data).toByteArray(); + hashBytes = new byte[MAX_BYTE_LENGTH]; + for (int i = 0; i < hashBytes.length; i++) { + hashBytes[i] = signPadding; + } + System.arraycopy( + rawValue, + 0, + hashBytes, + MAX_BYTE_LENGTH - rawValue.length, + rawValue.length); + } } else if (baseType.equals("string")) { hashBytes = ((String) data).getBytes(); } else if (baseType.equals("bytes")) { diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/Contracts.java b/app/src/main/java/com/alphawallet/app/web3j/ens/Contracts.java new file mode 100644 index 0000000000..7076608d12 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/Contracts.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.web3j.ens; + +import static com.alphawallet.ethereum.EthereumNetworkBase.GOERLI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; + +/** ENS registry contract addresses. */ +public class Contracts { + + public static final String MAINNET = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; + public static final String GOERLI = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; + + public static String resolveRegistryContract(long chainId) { + if (chainId == MAINNET_ID) { + return MAINNET; + } else if (chainId == GOERLI_ID) { + return GOERLI; + } else { + throw new EnsResolutionException( + "Unable to resolve ENS registry contract for network id: " + chainId); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/EnsException.java b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsException.java new file mode 100644 index 0000000000..5be79d87ee --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.web3j.ens; + +public class EnsException extends RuntimeException { + + public EnsException(Throwable cause) { + super(cause); + } + + public EnsException(String message) { + super(message); + } + + public EnsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/EnsGatewayRequestDTO.java b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsGatewayRequestDTO.java new file mode 100644 index 0000000000..c05f922ec4 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsGatewayRequestDTO.java @@ -0,0 +1,34 @@ +package com.alphawallet.app.web3j.ens; + +/* + * Copyright 2022 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +// https://eips.ethereum.org/EIPS/eip-3668#use-of-get-and-post-requests-for-the-gateway-interface +public class EnsGatewayRequestDTO { + + private String data; + + public EnsGatewayRequestDTO() {} + + public EnsGatewayRequestDTO(String data) { + this.data = data; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/EnsGatewayResponseDTO.java b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsGatewayResponseDTO.java new file mode 100644 index 0000000000..c41f7ed6da --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsGatewayResponseDTO.java @@ -0,0 +1,34 @@ +package com.alphawallet.app.web3j.ens; + +/* + * Copyright 2022 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +// https://eips.ethereum.org/EIPS/eip-3668#use-of-get-and-post-requests-for-the-gateway-interface +public class EnsGatewayResponseDTO { + + private String data; + + public EnsGatewayResponseDTO() {} + + public EnsGatewayResponseDTO(String data) { + this.data = data; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/EnsResolutionException.java b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsResolutionException.java new file mode 100644 index 0000000000..31a83fb362 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsResolutionException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.web3j.ens; + +/** ENS resolution exception. */ +public class EnsResolutionException extends EnsException { + public EnsResolutionException(String message) { + super(message); + } + + public EnsResolutionException(Throwable cause) { + super(cause); + } + + public EnsResolutionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/EnsUtils.java b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsUtils.java new file mode 100644 index 0000000000..28a83cd431 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/EnsUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.web3j.ens; + +import com.alphawallet.token.tools.Numeric; + +public class EnsUtils { + + public static final String EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; + + // Wildcard resolution + public static final byte[] ENSIP_10_INTERFACE_ID = Numeric.hexStringToByteArray("0x9061b923"); + public static final String EIP_3668_CCIP_INTERFACE_ID = "0x556f1830"; + + public static boolean isAddressEmpty(String address) { + return EMPTY_ADDRESS.equals(address); + } + + public static boolean isEIP3668(String data) { + if (data == null || data.length() < 10) { + return false; + } + + return EnsUtils.EIP_3668_CCIP_INTERFACE_ID.equals(data.substring(0, 10)); + } + + public static String getParent(String url) { + String ensUrl = url != null ? url.trim() : ""; + + if (ensUrl.equals(".") || !ensUrl.contains(".")) { + return null; + } + + return ensUrl.substring(ensUrl.indexOf(".") + 1); + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/NameHash.java b/app/src/main/java/com/alphawallet/app/web3j/ens/NameHash.java new file mode 100644 index 0000000000..2cea654eab --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/NameHash.java @@ -0,0 +1,130 @@ +/* + * Copyright 2019 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.web3j.ens; + +import android.text.TextUtils; +import org.web3j.crypto.Hash; +import org.web3j.utils.Numeric; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.IDN; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Locale; + +/** ENS name hash implementation. */ +public class NameHash +{ + private static final byte[] EMPTY = new byte[32]; + + public static byte[] nameHashAsBytes(String ensName) throws EnsResolutionException + { + return Numeric.hexStringToByteArray(nameHash(ensName)); + } + + public static String nameHash(String ensName) throws EnsResolutionException + { + String normalisedEnsName = normalise(ensName); + return Numeric.toHexString(nameHash(normalisedEnsName.split("\\."))); + } + + private static byte[] nameHash(String[] labels) + { + if (labels.length == 0 || labels[0].equals("")) + { + return EMPTY; + } + else + { + String[] tail; + if (labels.length == 1) + { + tail = new String[]{}; + } + else + { + tail = Arrays.copyOfRange(labels, 1, labels.length); + } + + byte[] remainderHash = nameHash(tail); + byte[] result = Arrays.copyOf(remainderHash, 64); + + byte[] labelHash = Hash.sha3(labels[0].getBytes(StandardCharsets.UTF_8)); + System.arraycopy(labelHash, 0, result, 32, labelHash.length); + + return Hash.sha3(result); + } + } + + /** + * Normalise ENS name as per the specification. + * + * @param ensName our user input ENS name + * @return normalised ens name + * @throws EnsResolutionException if the name cannot be normalised + */ + public static String normalise(String ensName) + { + try + { + return IDN.toASCII(ensName, IDN.USE_STD3_ASCII_RULES).toLowerCase(Locale.ROOT); + } + catch (Exception e) + { + throw new EnsResolutionException("Invalid ENS name provided: " + ensName); + } + } + + public static byte[] toUtf8Bytes(String string) + { + if (string == null || string.isEmpty()) + { + return null; + } + return string.getBytes(StandardCharsets.UTF_8); + } + + /** + * Encode Dns name. Reference implementation + * https://github.com/ethers-io/ethers.js/blob/fc1e006575d59792fa97b4efb9ea2f8cca1944cf/packages/hash/src.ts/namehash.ts#L49 + * + * @param name Dns name + * @return Encoded name in Hex format. + * @throws IOException + */ + public static String dnsEncode(String name) throws IOException + { + String[] parts = name.split("\\."); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (String part : parts) + { + if (TextUtils.isEmpty(part)) + { + break; + } + byte[] bytes = toUtf8Bytes("_" + normalise(part)); + if (bytes == null) + { + break; + } + bytes[0] = (byte) (bytes.length - 1); + + outputStream.write(bytes); + } + + return Numeric.toHexString(outputStream.toByteArray()) + "00"; + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/OffchainLookup.java b/app/src/main/java/com/alphawallet/app/web3j/ens/OffchainLookup.java new file mode 100644 index 0000000000..28c4264c8c --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/OffchainLookup.java @@ -0,0 +1,157 @@ +package com.alphawallet.app.web3j.ens; + +/* + * Copyright 2022 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +//package org.web3j.abi.datatypes.ens; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.DynamicArray; +import org.web3j.abi.datatypes.DynamicBytes; +import org.web3j.abi.datatypes.DynamicStruct; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Bytes4; +import org.web3j.utils.Numeric; + +/** https://eips.ethereum.org/EIPS/eip-3668#client-lookup-protocol */ +public class OffchainLookup extends DynamicStruct { + + private String sender; + private List urls; + private byte[] callData; + + /** + * Callback selector / Abi method Id + * org.web3j.abi.FunctionEncoder#buildMethodId(java.lang.String) + */ + private byte[] callbackFunction; + + private byte[] extraData; + + private static final List outputParameters = new ArrayList>(); + + static { + outputParameters.addAll( + Arrays.asList( + new TypeReference
() {}, + new TypeReference>() {}, + new TypeReference() {}, + new TypeReference() {}, + new TypeReference() {})); + } + + /*public OffchainLookup( + String sender, + List urls, + byte[] callData, + byte[] callbackFunction, + byte[] extraData) { + super( + new Address(sender), + new DynamicArray<>( + Utf8String.class, + urls.stream().map(Utf8String::new).collect(Collectors.toList())), + new DynamicBytes(callbackFunction), + new Bytes4(callData), + new DynamicBytes(extraData)); + this.sender = sender; + this.urls = urls; + this.callData = callData; + this.callbackFunction = callbackFunction; + this.extraData = extraData; + }*/ + + public OffchainLookup( + Address sender, + DynamicArray urls, + DynamicBytes callData, + Bytes4 callbackFunction, + DynamicBytes extraData) { + super(sender, urls, callData, callbackFunction, extraData); + this.sender = sender.getValue(); + this.urls = arrayToList(urls); + this.callData = callData.getValue(); + this.callbackFunction = callbackFunction.getValue(); + this.extraData = extraData.getValue(); + } + + private List arrayToList(DynamicArray values) + { + List valueList = new ArrayList<>(); + for (Utf8String val : values.getValue()) + { + valueList.add(val.getValue()); + } + + return valueList; + } + + public static OffchainLookup build(byte[] bytes) { + List resultList = + FunctionReturnDecoder.decode(Numeric.toHexString(bytes), outputParameters); + + return new OffchainLookup( + (Address) resultList.get(0), + (DynamicArray) resultList.get(1), + (DynamicBytes) resultList.get(2), + (Bytes4) resultList.get(3), + (DynamicBytes) resultList.get(4)); + } + + public String getSender() { + return sender; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public List getUrls() { + return urls; + } + + public void setUrls(List urls) { + this.urls = urls; + } + + public byte[] getCallData() { + return callData; + } + + public void setCallData(byte[] callData) { + this.callData = callData; + } + + public byte[] getCallbackFunction() { + return callbackFunction; + } + + public void setCallbackFunction(byte[] callbackFunction) { + this.callbackFunction = callbackFunction; + } + + public byte[] getExtraData() { + return extraData; + } + + public void setExtraData(byte[] extraData) { + this.extraData = extraData; + } +} diff --git a/app/src/main/java/com/alphawallet/app/web3j/ens/RecordTypes.java b/app/src/main/java/com/alphawallet/app/web3j/ens/RecordTypes.java new file mode 100644 index 0000000000..c3a64e68a4 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/web3j/ens/RecordTypes.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alphawallet.app.web3j.ens; + +import org.web3j.utils.Numeric; + +/** + * Record type interfaces supported by resolvers as per EIP-137 + */ +public class RecordTypes { + + public static final byte[] ADDR = Numeric.hexStringToByteArray("0x3b3b57de"); + public static final byte[] NAME = Numeric.hexStringToByteArray("0x691f3431"); + public static final byte[] ABI = Numeric.hexStringToByteArray("0x2203ab56"); + public static final byte[] PUB_KEY = Numeric.hexStringToByteArray("0xc8690233"); +} diff --git a/app/src/main/java/com/alphawallet/app/widget/ActionSheet.java b/app/src/main/java/com/alphawallet/app/widget/ActionSheet.java new file mode 100644 index 0000000000..11a3d05e83 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/widget/ActionSheet.java @@ -0,0 +1,53 @@ +package com.alphawallet.app.widget; + +import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; + +import android.content.Context; +import android.widget.FrameLayout; + +import androidx.activity.result.ActivityResult; +import androidx.annotation.NonNull; + +import com.alphawallet.app.entity.ActionSheetInterface; +import com.alphawallet.app.web3.entity.Web3Transaction; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; + +import java.math.BigInteger; + +/** + * Created by JB on 20/11/2022. + */ +public abstract class ActionSheet extends BottomSheetDialog implements ActionSheetInterface +{ + public ActionSheet(@NonNull Context context) + { + super(context); + } + + public void forceDismiss() + { + setOnDismissListener(v -> { + // Do nothing + }); + dismiss(); + } + + public void fullExpand() + { + FrameLayout bottomSheet = findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheet != null) BottomSheetBehavior.from(bottomSheet).setState(STATE_EXPANDED); + } + + public void lockDragging(boolean lock) + { + getBehavior().setDraggable(!lock); + + //ensure view fully expanded when locking scroll. Otherwise we may not be able to see our expanded view + if (lock) + { + FrameLayout bottomSheet = findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheet != null) BottomSheetBehavior.from(bottomSheet).setState(STATE_EXPANDED); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java b/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java index b0f057ae5d..f224c9fe1b 100644 --- a/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java +++ b/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java @@ -7,8 +7,6 @@ import android.content.SharedPreferences; import android.text.TextUtils; import android.view.View; -import android.widget.FrameLayout; -import android.widget.TextView; import androidx.activity.result.ActivityResult; import androidx.annotation.NonNull; @@ -23,6 +21,7 @@ import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TXSpeed; import com.alphawallet.app.entity.Transaction; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.SharedPreferenceRepository; @@ -37,9 +36,7 @@ import com.alphawallet.app.util.Utils; import com.alphawallet.app.walletconnect.entity.WCPeerMeta; import com.alphawallet.app.web3.entity.Web3Transaction; -import com.alphawallet.token.entity.Signable; import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.bottomsheet.BottomSheetDialog; import java.math.BigDecimal; import java.math.BigInteger; @@ -52,7 +49,7 @@ /** * Created by JB on 17/11/2020. */ -public class ActionSheetDialog extends BottomSheetDialog implements StandardFunctionInterface, ActionSheetInterface +public class ActionSheetDialog extends ActionSheet implements StandardFunctionInterface, ActionSheetInterface { private final BottomSheetToolbarView toolbar; private final GasWidget2 gasWidget; @@ -172,86 +169,6 @@ else if (activity instanceof WalletConnectActivity) setupCancelListeners(); } - public ActionSheetDialog(@NonNull Activity activity, ActionSheetCallback aCallback, SignAuthenticationCallback sCallback, Signable message) - { - super(activity); - setContentView(R.layout.dialog_action_sheet_sign); - - toolbar = findViewById(R.id.bottom_sheet_toolbar); - gasWidget = findViewById(R.id.gas_widgetx); - gasWidgetLegacy = findViewById(R.id.gas_widget_legacy); - balanceDisplay = findViewById(R.id.balance); - networkDisplay = findViewById(R.id.network_display_widget); - confirmationWidget = findViewById(R.id.confirmation_view); - addressDetail = findViewById(R.id.requester); - amountDisplay = findViewById(R.id.amount_display); - assetDetailView = findViewById(R.id.asset_detail); - functionBar = findViewById(R.id.layoutButtons); - detailWidget = null; - mode = ActionSheetMode.SIGN_MESSAGE; - callbackId = message.getCallbackId(); - this.activity = activity; - - actionSheetCallback = aCallback; - signCallback = sCallback; - - token = null; - tokensService = null; - candidateTransaction = null; - actionCompleted = false; - walletConnectRequestWidget = null; - gasWidgetInterface = null; - - addressDetail.setupRequester(message.getOrigin()); - SignDataWidget signWidget = findViewById(R.id.sign_widget); - signWidget.setupSignData(message); - signWidget.setLockCallback(this); - - toolbar.setTitle(Utils.getSigningTitle(message)); - - functionBar.setupFunctions(this, new ArrayList<>(Collections.singletonList(R.string.action_confirm))); - functionBar.revealButtons(); - setupCancelListeners(); - } - - public ActionSheetDialog(@NonNull Activity activity, ActionSheetCallback aCallback, int titleId, String message, int buttonTextId, - long cId, Token baseToken) - { - super(activity); - setContentView(R.layout.dialog_action_sheet_message); - - toolbar = findViewById(R.id.bottom_sheet_toolbar); - TextView messageView = findViewById(R.id.text_message); - functionBar = findViewById(R.id.layoutButtons); - this.activity = activity; - - actionSheetCallback = aCallback; - mode = ActionSheetMode.MESSAGE; - - toolbar.setTitle(titleId); - messageView.setText(message); - - gasWidget = null; - balanceDisplay = null; - networkDisplay = null; - confirmationWidget = null; - addressDetail = null; - amountDisplay = null; - assetDetailView = null; - detailWidget = null; - callbackId = cId; - token = baseToken; - tokensService = null; - candidateTransaction = null; - walletConnectRequestWidget = null; - gasWidgetLegacy = null; - gasWidgetInterface = null; - - functionBar.setupFunctions(this, new ArrayList<>(Collections.singletonList(buttonTextId))); - functionBar.revealButtons(); - setupCancelListeners(); - } - // wallet connect request public ActionSheetDialog(Activity activity, WCPeerMeta wcPeerMeta, long chainIdOverride, String iconUrl, ActionSheetCallback actionSheetCallback) { @@ -336,30 +253,6 @@ public ActionSheetDialog(Activity activity, ActionSheetCallback aCallback, int t setupCancelListeners(); } - private GasWidgetInterface setupGasWidget() - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean canUse1559Transactions = prefs.getBoolean(SharedPreferenceRepository.EXPERIMENTAL_1559_TX, false); - - use1559Transactions = canUse1559Transactions && has1559Gas() //1559 Transactions toggled on in settings and this chain supports 1559 - && !(token.isEthereum() && candidateTransaction.leafPosition == -2) //User not sweeping wallet (if so we need to use legacy tx) - && !tokensService.hasLockedGas(token.tokenInfo.chainId) //Service has locked gas, can only use legacy (eg Optimism). - && !candidateTransaction.isConstructor(); //Currently cannot use EIP1559 for constructors due to gas calculation issues - - if (use1559Transactions) - { - gasWidget.setupWidget(tokensService, token, candidateTransaction, actionSheetCallback.gasSelectLauncher()); - return gasWidget; - } - else - { - gasWidget.setVisibility(View.GONE); - gasWidgetLegacy.setVisibility(View.VISIBLE); - gasWidgetLegacy.setupWidget(tokensService, token, candidateTransaction, this, actionSheetCallback.gasSelectLauncher()); - return gasWidgetLegacy; - } - } - public ActionSheetDialog(Activity activity, ActionSheetMode mode) { super(activity); @@ -389,6 +282,30 @@ public ActionSheetDialog(Activity activity, ActionSheetMode mode) callbackId = 0; } + private GasWidgetInterface setupGasWidget() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + boolean canUse1559Transactions = prefs.getBoolean(SharedPreferenceRepository.EXPERIMENTAL_1559_TX, false); + + use1559Transactions = canUse1559Transactions && has1559Gas() //1559 Transactions toggled on in settings and this chain supports 1559 + && !(token.isEthereum() && candidateTransaction.leafPosition == -2) //User not sweeping wallet (if so we need to use legacy tx) + && !tokensService.hasLockedGas(token.tokenInfo.chainId) //Service has locked gas, can only use legacy (eg Optimism). + && !candidateTransaction.isConstructor(); //Currently cannot use EIP1559 for constructors due to gas calculation issues + + if (use1559Transactions) + { + gasWidget.setupWidget(tokensService, token, candidateTransaction, actionSheetCallback.gasSelectLauncher()); + return gasWidget; + } + else + { + gasWidget.setVisibility(View.GONE); + gasWidgetLegacy.setVisibility(View.VISIBLE); + gasWidgetLegacy.setupWidget(tokensService, token, candidateTransaction, this, actionSheetCallback.gasSelectLauncher()); + return gasWidgetLegacy; + } + } + public void setSignOnly() { //sign only, and return signature to process @@ -405,8 +322,8 @@ public void setURL(String url) { AddressDetailView requester = findViewById(R.id.requester); requester.setupRequester(url); - detailWidget.setupTransaction(candidateTransaction, token.tokenInfo.chainId, tokensService.getCurrentAddress(), - tokensService.getNetworkSymbol(token.tokenInfo.chainId), this); + setupTransactionDetails(); + if (candidateTransaction.isConstructor()) { addressDetail.setVisibility(View.GONE); @@ -427,9 +344,18 @@ private void setupTransactionDetails() { detailWidget.setupTransaction(candidateTransaction, token.tokenInfo.chainId, tokensService.getCurrentAddress(), tokensService.getNetworkSymbol(token.tokenInfo.chainId), this); - detailWidget.setVisibility(View.VISIBLE); + + if (candidateTransaction.isBaseTransfer()) + { + detailWidget.setVisibility(View.GONE); + } + else + { + detailWidget.setVisibility(View.VISIBLE); + } } + @Override public void setCurrentGasIndex(ActivityResult result) { if (result == null || result.getData() == null) return; @@ -486,9 +412,6 @@ public void handleClick(String action, int id) sendTransaction(); } break; - case SIGN_MESSAGE: - signMessage(); - break; case SIGN_TRANSACTION: signTransaction(); break; @@ -507,8 +430,6 @@ public void handleClick(String action, int id) } break; } - - actionSheetCallback.notifyConfirm(mode.toString()); } private BigDecimal getTransactionAmount() @@ -536,43 +457,6 @@ private String getERC721TokenId() return token.getTransferValueRaw(transaction.transactionInput).toString(); } - private void signMessage() - { - //get authentication - functionBar.setVisibility(View.GONE); - - //authentication screen - SignAuthenticationCallback localSignCallback = new SignAuthenticationCallback() - { - final SignDataWidget signWidget = findViewById(R.id.sign_widget); - - @Override - public void gotAuthorisation(boolean gotAuth) - { - actionCompleted = true; - //display success and hand back to calling function - if (gotAuth) - { - confirmationWidget.startProgressCycle(1); - signCallback.gotAuthorisationForSigning(gotAuth, signWidget.getSignable()); - } - else - { - cancelAuthentication(); - } - } - - @Override - public void cancelAuthentication() - { - confirmationWidget.hide(); - signCallback.gotAuthorisationForSigning(false, signWidget.getSignable()); - } - }; - - actionSheetCallback.getAuthorisation(localSignCallback); - } - /** * Popup a dialogbox to ask user if they really want to try to send this transaction, * as we calculate it will fail due to insufficient gas. User knows best though. @@ -683,6 +567,7 @@ public void gotAuthorisation(boolean gotAuth) confirmationWidget.startProgressCycle(4); //send the transaction actionSheetCallback.signTransaction(formTransaction()); + actionSheetCallback.notifyConfirm(mode.getValue()); } @Override @@ -775,8 +660,8 @@ public void gotAuthorisation(boolean gotAuth) return; } confirmationWidget.startProgressCycle(4); - //send the transaction actionSheetCallback.sendTransaction(formTransaction()); + actionSheetCallback.notifyConfirm(mode.getValue()); } @Override @@ -790,26 +675,6 @@ public void cancelAuthentication() actionSheetCallback.getAuthorisation(signCallback); } - @Override - public void lockDragging(boolean lock) - { - getBehavior().setDraggable(!lock); - - //ensure view fully expanded when locking scroll. Otherwise we may not be able to see our expanded view - if (lock) - { - FrameLayout bottomSheet = findViewById(com.google.android.material.R.id.design_bottom_sheet); - if (bottomSheet != null) BottomSheetBehavior.from(bottomSheet).setState(STATE_EXPANDED); - } - } - - @Override - public void fullExpand() - { - FrameLayout bottomSheet = findViewById(com.google.android.material.R.id.design_bottom_sheet); - if (bottomSheet != null) BottomSheetBehavior.from(bottomSheet).setState(STATE_EXPANDED); - } - //Takes gas estimate from calling activity (eg WalletConnectActivity) and updates dialog public void setGasEstimate(BigInteger estimate) { @@ -834,14 +699,6 @@ public void success() } } - public void forceDismiss() - { - setOnDismissListener(v -> { - // Do nothing - }); - dismiss(); - } - public void waitForEstimate() { functionBar.setPrimaryButtonWaiting(); @@ -877,4 +734,13 @@ private boolean has1559Gas() return false; } + + public void setSigningWallet(String address) + { + SharedPreferenceRepository prefs = new SharedPreferenceRepository(getContext()); + if (!prefs.getCurrentWalletAddress().equalsIgnoreCase(address)) + { + addressDetail.addMessage(getContext().getString(R.string.message_wc_wallet_different_from_active_wallet), R.drawable.ic_red_warning); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/ActionSheetMode.java b/app/src/main/java/com/alphawallet/app/widget/ActionSheetMode.java deleted file mode 100644 index 75403efff8..0000000000 --- a/app/src/main/java/com/alphawallet/app/widget/ActionSheetMode.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.alphawallet.app.widget; - -/** - * Created by JB on 12/01/2021. - */ -public enum ActionSheetMode -{ - SEND_TRANSACTION, - SEND_TRANSACTION_DAPP, - SEND_TRANSACTION_WC, - SIGN_MESSAGE, - SIGN_TRANSACTION, - SPEEDUP_TRANSACTION, - CANCEL_TRANSACTION, - MESSAGE, - WALLET_CONNECT_REQUEST, - NODE_STATUS_INFO -} diff --git a/app/src/main/java/com/alphawallet/app/widget/ActionSheetSignDialog.java b/app/src/main/java/com/alphawallet/app/widget/ActionSheetSignDialog.java new file mode 100644 index 0000000000..19cbfc05cc --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/widget/ActionSheetSignDialog.java @@ -0,0 +1,163 @@ +package com.alphawallet.app.widget; + +import android.app.Activity; +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.core.util.Pair; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.SignAuthenticationCallback; +import com.alphawallet.app.entity.StandardFunctionInterface; +import com.alphawallet.app.entity.analytics.ActionSheetMode; +import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.viewmodel.SignDialogViewModel; +import com.alphawallet.token.entity.Signable; +import com.bumptech.glide.Glide; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Created by JB on 20/11/2022. + */ +public class ActionSheetSignDialog extends ActionSheet implements StandardFunctionInterface, SignAuthenticationCallback +{ + private final SignDialogViewModel viewModel; + private final BottomSheetToolbarView toolbar; + private final ConfirmationWidget confirmationWidget; + private final AddressDetailView requesterDetail; + private final AddressDetailView addressDetail; + private final FunctionButtonBar functionBar; + private final ActionSheetCallback actionSheetCallback; + private final Activity activity; + private final long callbackId; + private boolean actionCompleted; + + public ActionSheetSignDialog(@NonNull Activity callingActivity, ActionSheetCallback aCallback, Signable message) + { + super(callingActivity); + View view = View.inflate(callingActivity, R.layout.dialog_action_sheet_sign, null); + setContentView(view); + toolbar = findViewById(R.id.bottom_sheet_toolbar); + confirmationWidget = findViewById(R.id.confirmation_view); + requesterDetail = findViewById(R.id.requester); + addressDetail = findViewById(R.id.wallet); + functionBar = findViewById(R.id.layoutButtons); + callbackId = message.getCallbackId(); + activity = callingActivity; + + actionSheetCallback = aCallback; + + requesterDetail.setupRequester(message.getOrigin()); + SignDataWidget signWidget = findViewById(R.id.sign_widget); + signWidget.setupSignData(message); + + toolbar.setTitle(Utils.getSigningTitle(message)); + + functionBar.setupFunctions(this, new ArrayList<>(Collections.singletonList(R.string.action_confirm))); + functionBar.revealButtons(); + setupCancelListeners(); + actionCompleted = false; + + //ensure view fully expanded when locking scroll. Otherwise we may not be able to see our expanded view + fullExpand(); + + viewModel = new ViewModelProvider((ViewModelStoreOwner) activity).get(SignDialogViewModel.class); + viewModel.completed().observe((LifecycleOwner) activity, this::signComplete); + viewModel.message().observe((LifecycleOwner) activity, this::onMessage); + setCanceledOnTouchOutside(false); + } + + @Override + public void setIcon(String icon) + { + ImageView iconView = findViewById(R.id.logo); + Glide.with(activity) + .load(icon) + .circleCrop() + .into(iconView); + } + + @Override + public void handleClick(String action, int id) + { + //get authentication + functionBar.setVisibility(View.GONE); + viewModel.getAuthentication(activity, this); + } + + // Set for locked signing account, which WalletConnect v2 requires + @Override + public void setSigningWallet(String account) + { + viewModel.setSigningWallet(account); + addressDetail.setVisibility(View.VISIBLE); + addressDetail.setupAddress(account, "", null); + } + + private void onMessage(Pair res) + { + addressDetail.addMessage(getContext().getString(res.first), res.second); + } + + public void success() + { + if (isShowing() && confirmationWidget != null && confirmationWidget.isShown()) + { + confirmationWidget.completeProgressMessage(".", this::dismiss); + } + } + + private void setupCancelListeners() + { + toolbar.setCloseListener(v -> dismiss()); + + setOnDismissListener(v -> { + actionSheetCallback.dismissed("", callbackId, actionCompleted); + }); + } + + @Override + public void gotAuthorisation(boolean gotAuth) + { + final SignDataWidget signWidget = findViewById(R.id.sign_widget); + if (gotAuth) + { + //start animation + confirmationWidget.startProgressCycle(1); + actionSheetCallback.notifyConfirm(ActionSheetMode.SIGN_MESSAGE.getValue()); + viewModel.signMessage(signWidget.getSignable(), actionSheetCallback); + } + else + { + Toast.makeText(activity, activity.getString(R.string.error_while_signing_transaction), Toast.LENGTH_SHORT).show(); + cancelAuthentication(); + } + } + + @Override + public void cancelAuthentication() + { + dismiss(); + } + + private void signComplete(Boolean success) + { + if (success) + { + actionCompleted = true; + success(); + } + else + { + dismiss(); + } + } +} diff --git a/app/src/main/java/com/alphawallet/app/widget/ActivityHistoryList.java b/app/src/main/java/com/alphawallet/app/widget/ActivityHistoryList.java index eaaca03c18..75d88c4547 100644 --- a/app/src/main/java/com/alphawallet/app/widget/ActivityHistoryList.java +++ b/app/src/main/java/com/alphawallet/app/widget/ActivityHistoryList.java @@ -176,7 +176,9 @@ private RealmQuery getEthListener(long chainId, Wallet wallet, { return realm.where(RealmTransaction.class) .sort("timeStamp", Sort.DESCENDING) - .equalTo("input", "0x") + .beginGroup() + .equalTo("input", "0x").or().equalTo("input", "") + .endGroup() .beginGroup() .equalTo("to", wallet.address, Case.INSENSITIVE) .or() diff --git a/app/src/main/java/com/alphawallet/app/widget/AddressBar.java b/app/src/main/java/com/alphawallet/app/widget/AddressBar.java new file mode 100644 index 0000000000..74d82b0118 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/widget/AddressBar.java @@ -0,0 +1,367 @@ +package com.alphawallet.app.widget; + +import static com.alphawallet.app.util.KeyboardUtils.showKeyboard; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.webkit.WebBackForwardList; +import android.widget.AutoCompleteTextView; +import android.widget.ImageView; + +import androidx.annotation.Nullable; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.DApp; +import com.alphawallet.app.ui.widget.adapter.DappBrowserSuggestionsAdapter; +import com.alphawallet.app.util.DappBrowserUtils; +import com.alphawallet.app.util.KeyboardUtils; +import com.google.android.material.appbar.MaterialToolbar; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; + +public class AddressBar extends MaterialToolbar +{ + private final int ANIMATION_DURATION = 100; + + private AutoCompleteTextView urlTv; + private DappBrowserSuggestionsAdapter adapter; + private AddressBarListener listener; + private ImageView btnClear; + private View layoutNavigation; + private ImageView back; + private ImageView next; + private ImageView home; + + @Nullable + private Disposable disposable; + private boolean focused; + + public AddressBar(Context context, AttributeSet attributeSet) + { + super(context, attributeSet); + inflate(context, R.layout.layout_url_bar_full, this); + + initView(); + } + + public void setup(List list, AddressBarListener listener) + { + adapter = new DappBrowserSuggestionsAdapter( + getContext(), + list, + this::load + ); + this.listener = listener; + urlTv.setAdapter(null); + + urlTv.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_GO) + { + load(urlTv.getText().toString()); + } + return false; + }); + + // Both these are required, the onFocus listener is required to respond to the first click. + urlTv.setOnFocusChangeListener((v, hasFocus) -> { + //see if we have focus flag + if (hasFocus && focused) openURLInputView(); + }); + + urlTv.setOnClickListener(v -> { + openURLInputView(); + }); + + urlTv.setShowSoftInputOnFocus(true); + + urlTv.setOnLongClickListener(v -> { + urlTv.dismissDropDown(); + return false; + }); + + urlTv.addTextChangedListener(new TextWatcher() + { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) + { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) + { + + } + + @Override + public void afterTextChanged(Editable editable) + { + adapter.setHighlighted(editable.toString()); + } + }); + } + + private void load(String url) + { + listener.onLoad(url); + expandCollapseView(layoutNavigation, true); + leaveEditMode(); + } + + private void initView() + { + urlTv = findViewById(R.id.url_tv); + home = findViewById(R.id.home); + if (home != null) home.setOnClickListener(v -> { + disableNavigationButtons(); + WebBackForwardList backForwardList = listener.onHomePagePressed(); + updateNavigationButtons(backForwardList); + }); + + btnClear = findViewById(R.id.clear_url); + btnClear.setOnClickListener(v -> { + clearAddressBar(); + }); + + layoutNavigation = findViewById(R.id.layout_navigator); + back = findViewById(R.id.back); + back.setOnClickListener(v -> { + disableNavigationButtons(); + WebBackForwardList backForwardList = listener.loadPrevious(); + updateNavigationButtons(backForwardList); + }); + + next = findViewById(R.id.next); + next.setOnClickListener(v -> { + disableNavigationButtons(); + WebBackForwardList backForwardList = listener.loadNext(); + updateNavigationButtons(backForwardList); + }); + } + + private void clearAddressBar() + { + if (urlTv.getText().toString().isEmpty()) + { + KeyboardUtils.hideKeyboard(urlTv); + listener.onClear(); + } + else + { + urlTv.getText().clear(); + openURLInputView(); + showKeyboard(urlTv); //ensure keyboard shows here so we can listen for it being cancelled + } + } + + private void openURLInputView() + { + urlTv.setAdapter(null); + expandCollapseView(layoutNavigation, false); + + disposable = Observable.zip( + Observable.interval(600, TimeUnit.MILLISECONDS).take(1), + Observable.fromArray(btnClear), (interval, item) -> item) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(this::postBeginSearchSession); + } + + private void postBeginSearchSession(@NotNull ImageView item) + { + urlTv.setAdapter(adapter); + urlTv.showDropDown(); + if (item.getVisibility() == View.GONE) + { + expandCollapseView(item, true); + showKeyboard(urlTv); + } + } + + private synchronized void expandCollapseView(@NotNull View view, boolean expandView) + { + //detect if view is expanded or collapsed + boolean isViewExpanded = view.getVisibility() == View.VISIBLE; + + //Collapse view + if (isViewExpanded && !expandView) + { + int finalWidth = view.getWidth(); + ValueAnimator valueAnimator = slideAnimator(finalWidth, 0, view); + valueAnimator.addListener(new Animator.AnimatorListener() + { + @Override + public void onAnimationStart(Animator animator) + { + + } + + @Override + public void onAnimationEnd(Animator animator) + { + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animator) + { + + } + + @Override + public void onAnimationRepeat(Animator animator) + { + + } + }); + valueAnimator.start(); + } + //Expand view + else if (!isViewExpanded && expandView) + { + view.setVisibility(View.VISIBLE); + + int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + + view.measure(widthSpec, heightSpec); + int width = view.getMeasuredWidth(); + ValueAnimator valueAnimator = slideAnimator(0, width, view); + valueAnimator.start(); + } + } + + @NotNull + private ValueAnimator slideAnimator(int start, int end, final View view) + { + + final ValueAnimator animator = ValueAnimator.ofInt(start, end); + + animator.addUpdateListener(valueAnimator -> { + // Update Height + int value = (Integer) valueAnimator.getAnimatedValue(); + + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.width = value; + view.setLayoutParams(layoutParams); + }); + animator.setDuration(ANIMATION_DURATION); + return animator; + } + + public void removeSuggestion(DApp dApp) + { + adapter.removeSuggestion(dApp); + } + + public void addSuggestion(DApp dapp) + { + adapter.addSuggestion(dapp); + } + + public void shrinkSearchBar() + { + expandCollapseView(layoutNavigation, true); + btnClear.setVisibility(View.GONE); + urlTv.dismissDropDown(); + } + + public void destroy() + { + if (disposable != null && !disposable.isDisposed()) disposable.dispose(); + } + + public void clear() + { + if (urlTv != null) + urlTv.getText().clear(); + } + + public void leaveEditMode() + { + if (urlTv != null) + { + urlTv.clearFocus(); + KeyboardUtils.hideKeyboard(urlTv); + btnClear.setVisibility(GONE); + } + focused = true; + } + + public void leaveFocus() + { + if (urlTv != null) urlTv.clearFocus(); + focused = false; + } + + public void setUrl(String newUrl) + { + if (urlTv != null) + urlTv.setText(newUrl); + } + + public void updateNavigationButtons(WebBackForwardList backForwardList) + { + boolean isLast = backForwardList.getCurrentIndex() + 1 > backForwardList.getSize() - 1; + if (isLast) + { + disableButton(next); + } + else + { + enableButton(next); + } + + boolean isFirst = backForwardList.getCurrentIndex() == 0; + if (isFirst) + { + disableButton(back); + } + else + { + enableButton(back); + } + } + + public boolean isOnHomePage() + { + return DappBrowserUtils.isDefaultDapp(urlTv.getText().toString()); + } + + public String getUrl() + { + return urlTv.getText().toString(); + } + + private void disableNavigationButtons() + { + disableButton(back); + disableButton(next); + } + + private void enableButton(ImageView button) + { + button.setEnabled(true); + button.setAlpha(1.0f); + } + + private void disableButton(ImageView button) + { + button.setEnabled(false); + button.setAlpha(0.3f); + } +} diff --git a/app/src/main/java/com/alphawallet/app/widget/AddressBarListener.java b/app/src/main/java/com/alphawallet/app/widget/AddressBarListener.java new file mode 100644 index 0000000000..9777710439 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/widget/AddressBarListener.java @@ -0,0 +1,16 @@ +package com.alphawallet.app.widget; + +import android.webkit.WebBackForwardList; + +public interface AddressBarListener +{ + boolean onLoad(String urlText); + + void onClear(); + + WebBackForwardList loadNext(); + + WebBackForwardList loadPrevious(); + + WebBackForwardList onHomePagePressed(); +} diff --git a/app/src/main/java/com/alphawallet/app/widget/AddressDetailView.java b/app/src/main/java/com/alphawallet/app/widget/AddressDetailView.java index c9c6c5069e..7f16949cda 100644 --- a/app/src/main/java/com/alphawallet/app/widget/AddressDetailView.java +++ b/app/src/main/java/com/alphawallet/app/widget/AddressDetailView.java @@ -5,7 +5,6 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; -import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -24,10 +23,13 @@ public class AddressDetailView extends LinearLayout { private final TextView textAddressSummary; private final TextView textFullAddress; + private final TextView labelEnsName; private final TextView textEnsName; + private final TextView textMessage; private final ImageView recipientDetails; private final UserAvatar userAvatar; private final LinearLayout layoutDetails; + private final LinearLayout layoutEnsName; private final LinearLayout layoutHolder; public AddressDetailView(Context context, @Nullable AttributeSet attrs) @@ -36,10 +38,13 @@ public AddressDetailView(Context context, @Nullable AttributeSet attrs) inflate(context, R.layout.item_address_detail, this); textAddressSummary = findViewById(R.id.text_recipient); textFullAddress = findViewById(R.id.text_recipient_address); + labelEnsName = findViewById(R.id.label_ens_name); textEnsName = findViewById(R.id.text_ens_name); + textMessage = findViewById(R.id.message); recipientDetails = findViewById(R.id.image_more); userAvatar = findViewById(R.id.blockie); layoutDetails = findViewById(R.id.layout_detail); + layoutEnsName = findViewById(R.id.layout_ens_name); layoutHolder = findViewById(R.id.layout_holder); getAttrs(context, attrs); } @@ -55,18 +60,45 @@ private void getAttrs(Context context, AttributeSet attrs) recipientText.setText(a.getResourceId(R.styleable.InputView_label, R.string.recipient)); } + public void addMessage(String message, int drawableRes) + { + textMessage.setText(message); + textMessage.setVisibility(View.VISIBLE); + if (drawableRes > 0) + { + textAddressSummary.setCompoundDrawablesWithIntrinsicBounds(drawableRes, 0, 0, 0); + textMessage.setCompoundDrawablesWithIntrinsicBounds(drawableRes, 0, 0, 0); + } + } + public void setupAddress(String address, String ensName, Token destToken) { - String destStr = (!TextUtils.isEmpty(ensName) ? ensName + " | " : "") + Utils.formatAddress(address); + boolean hasEns = !TextUtils.isEmpty(ensName); + String destStr = (hasEns ? ensName + " | " : "") + Utils.formatAddress(address); textAddressSummary.setText(destStr); userAvatar.bind(new Wallet(address), wallet -> { /*NOP, here to enable lookup of ENS avatar*/ }); textFullAddress.setText(address); - textEnsName.setText(ensName); - if (TextUtils.isEmpty(ensName) && destToken != null && !destToken.isEthereum()) + if (TextUtils.isEmpty(ensName)) + { + if (destToken != null && !destToken.isEthereum()) + { + labelEnsName.setVisibility(View.VISIBLE); + layoutEnsName.setVisibility(View.VISIBLE); + labelEnsName.setText(R.string.token_text); + textEnsName.setText(destToken.getFullName()); + } + else + { + labelEnsName.setVisibility(View.GONE); + layoutEnsName.setVisibility(View.GONE); + } + } + else { - ((TextView)findViewById(R.id.label_ens)).setText(R.string.token_text); - textEnsName.setText(destToken.getFullName()); + labelEnsName.setVisibility(View.VISIBLE); + layoutEnsName.setVisibility(View.VISIBLE); + textEnsName.setText(ensName); } layoutHolder.setOnClickListener(v -> { @@ -88,12 +120,10 @@ public void setupAddress(String address, String ensName, Token destToken) public void setupRequester(String requesterUrl) { setVisibility(View.VISIBLE); - recipientDetails.setVisibility(View.GONE); + recipientDetails.setVisibility(View.INVISIBLE); //shorten requesterURL if required requesterUrl = abbreviateURL(requesterUrl); textAddressSummary.setText(requesterUrl); - ViewGroup.LayoutParams param = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 3.4f); - textAddressSummary.setLayoutParams(param); } private String abbreviateURL(String inputURL) @@ -101,7 +131,7 @@ private String abbreviateURL(String inputURL) if (inputURL.length() > 32) { int index = inputURL.indexOf("/", 20); - return index >= 0 ? inputURL.substring(0,index) : inputURL; + return index >= 0 ? inputURL.substring(0, index) : inputURL; } else { diff --git a/app/src/main/java/com/alphawallet/app/widget/BuyEthOptionsView.java b/app/src/main/java/com/alphawallet/app/widget/BuyEthOptionsView.java new file mode 100644 index 0000000000..502496bcec --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/widget/BuyEthOptionsView.java @@ -0,0 +1,64 @@ +package com.alphawallet.app.widget; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.LayoutRes; + +import com.alphawallet.app.R; + + +public class BuyEthOptionsView extends FrameLayout implements View.OnClickListener +{ + private OnClickListener onBuyWithCoinbasePayListener; + private OnClickListener onBuyWithRampListener; + + public BuyEthOptionsView(Context context) + { + this(context, R.layout.dialog_buy_eth_options); + } + + public BuyEthOptionsView(Context context, @LayoutRes int layoutId) + { + super(context); + init(layoutId); + } + + private void init(@LayoutRes int layoutId) + { + LayoutInflater.from(getContext()).inflate(layoutId, this, true); + findViewById(R.id.buy_with_coinbase_pay).setOnClickListener(this); + findViewById(R.id.buy_with_ramp).setOnClickListener(this); + } + + @Override + public void onClick(View view) + { + if (view.getId() == R.id.buy_with_coinbase_pay) + { + if (onBuyWithCoinbasePayListener != null) + { + onBuyWithCoinbasePayListener.onClick(view); + } + } + else if (view.getId() == R.id.buy_with_ramp) + { + if (onBuyWithRampListener != null) + { + onBuyWithRampListener.onClick(view); + } + } + } + + public void setOnBuyWithCoinbasePayListener(OnClickListener onClickListener) + { + this.onBuyWithCoinbasePayListener = onClickListener; + } + + public void setOnBuyWithRampListener(OnClickListener onClickListener) + { + this.onBuyWithRampListener = onClickListener; + } +} diff --git a/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java b/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java index f48eb5a960..1bcadfd09c 100644 --- a/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java +++ b/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java @@ -2,15 +2,16 @@ import android.app.Activity; import android.content.Context; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; +import androidx.annotation.Nullable; + +import com.alphawallet.app.R; import com.alphawallet.token.entity.SigReturnType; import com.alphawallet.token.entity.XMLDsigDescriptor; -import com.alphawallet.app.R; import com.google.android.material.appbar.MaterialToolbar; public class CertifiedToolbarView extends MaterialToolbar @@ -18,6 +19,7 @@ public class CertifiedToolbarView extends MaterialToolbar private Activity activity; private AWalletAlertDialog dialog; private final ProgressBar downloadSpinner; + private final ProgressBar syncSpinner; private int lockResource = 0; public CertifiedToolbarView(Context ctx, @Nullable AttributeSet attrs) @@ -25,6 +27,7 @@ public CertifiedToolbarView(Context ctx, @Nullable AttributeSet attrs) super(ctx, attrs); inflate(ctx, R.layout.layout_certified_toolbar, this); downloadSpinner = findViewById(R.id.cert_progress_spinner); + syncSpinner = findViewById(R.id.nft_scan_spinner); } public void onSigData(final XMLDsigDescriptor sigData, final Activity act) @@ -124,4 +127,14 @@ public void stopDownload() { downloadSpinner.setVisibility(View.GONE); } + + public void showNFTSync() + { + syncSpinner.setVisibility(View.VISIBLE); + } + + public void nftSyncComplete() + { + syncSpinner.setVisibility(View.GONE); + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/ConfirmationWidget.java b/app/src/main/java/com/alphawallet/app/widget/ConfirmationWidget.java index 38804d0f49..f9ef088699 100644 --- a/app/src/main/java/com/alphawallet/app/widget/ConfirmationWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/ConfirmationWidget.java @@ -3,6 +3,7 @@ import android.animation.Animator; import android.content.Context; import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -27,7 +28,7 @@ public class ConfirmationWidget extends RelativeLayout private final TextView hashText; private final RelativeLayout progressLayout; private RealmResults realmTransactionUpdates; - private final Handler handler = new Handler(); + private final Handler handler = new Handler(Looper.getMainLooper()); public ConfirmationWidget(Context context, AttributeSet attrs) { @@ -56,9 +57,11 @@ public void startAnimate(long expectedTransactionTime, Realm transactionRealm, S public void startProgressCycle(int cycleTime) { - progress.setVisibility(View.VISIBLE); - progressLayout.setVisibility(View.VISIBLE); - progress.startAnimation(cycleTime); + handler.post(() -> { + progress.setVisibility(View.VISIBLE); + progressLayout.setVisibility(View.VISIBLE); + progress.startAnimation(cycleTime); + }); } public void completeProgressMessage(String message, final ProgressCompleteCallback callback) @@ -78,27 +81,29 @@ public void onAnimationCancel(Animator animation) { } public void onAnimationRepeat(Animator animation) { } }; - if (!TextUtils.isEmpty(message)) - { - completeProgressSuccess(true); - hashText.setVisibility(View.VISIBLE); - hashText.setAlpha(1.0f); - if (message.length() > 1) hashText.setText(message); - - hashText.animate() - .alpha(0.0f) - .setDuration(1500) - .setListener(animatorListener); - } - else - { - completeProgressSuccess(false); - hashText.setVisibility(View.GONE); - hashText.animate() - .alpha(0.0f) - .setDuration(1500) - .setListener(animatorListener); - } + handler.post(() -> { + if (!TextUtils.isEmpty(message)) + { + completeProgressSuccess(true); + hashText.setVisibility(View.VISIBLE); + hashText.setAlpha(1.0f); + if (message.length() > 1) hashText.setText(message); + + hashText.animate() + .alpha(0.0f) + .setDuration(1500) + .setListener(animatorListener); + } + else + { + completeProgressSuccess(false); + hashText.setVisibility(View.GONE); + hashText.animate() + .alpha(0.0f) + .setDuration(1500) + .setListener(animatorListener); + } + }); } //listen for transaction completion diff --git a/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java b/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java index b07fdf4aae..a6ae83075d 100644 --- a/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java +++ b/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java @@ -14,8 +14,8 @@ import com.alphawallet.app.R; import com.google.android.material.button.MaterialButton; -public class CopyTextView extends LinearLayout { - +public class CopyTextView extends LinearLayout +{ public static final String KEY_ADDRESS = "key_address"; private final Context context; @@ -23,6 +23,7 @@ public class CopyTextView extends LinearLayout { private int textResId; private int gravity; + private int lines; private boolean showToast; private boolean boldFont; private boolean removePadding; @@ -56,6 +57,7 @@ private void getAttrs(Context context, AttributeSet attrs) boldFont = a.getBoolean(R.styleable.CopyTextView_bold, false); removePadding = a.getBoolean(R.styleable.CopyTextView_removePadding, false); marginRight = a.getDimension(R.styleable.CopyTextView_marginRight, 0.0f); + lines = a.getInt(R.styleable.CopyTextView_lines, 1); } finally { @@ -65,7 +67,17 @@ private void getAttrs(Context context, AttributeSet attrs) private void bindViews() { - button = findViewById(R.id.button); + if (lines == 2) + { + button = findViewById(R.id.button_address); + findViewById(R.id.button).setVisibility(View.GONE); + button.setVisibility(View.VISIBLE); + } + else + { + button = findViewById(R.id.button); + } + setText(getContext().getString(textResId)); button.setOnClickListener(v -> copyToClipboard()); } @@ -98,6 +110,8 @@ private void copyToClipboard() } if (showToast) + { Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } } } diff --git a/app/src/main/java/com/alphawallet/app/widget/EmailPromptView.java b/app/src/main/java/com/alphawallet/app/widget/EmailPromptView.java index 918969906f..82ed5c7365 100644 --- a/app/src/main/java/com/alphawallet/app/widget/EmailPromptView.java +++ b/app/src/main/java/com/alphawallet/app/widget/EmailPromptView.java @@ -1,6 +1,5 @@ package com.alphawallet.app.widget; -import android.app.Activity; import android.content.Context; import android.os.Handler; import android.text.InputType; @@ -8,16 +7,14 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.widget.FrameLayout; import android.widget.LinearLayout; import androidx.annotation.LayoutRes; import com.alphawallet.app.R; import com.alphawallet.app.entity.StandardFunctionInterface; -import com.alphawallet.app.ui.HomeActivity; +import com.alphawallet.app.repository.KeyProviderFactory; import com.alphawallet.app.util.KeyboardUtils; -import com.alphawallet.app.util.Utils; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.mailchimp.sdk.api.model.Contact; import com.mailchimp.sdk.api.model.ContactStatus; @@ -31,10 +28,6 @@ public class EmailPromptView extends LinearLayout implements StandardFunctionInterface { - static { - System.loadLibrary("keys"); - } - private BottomSheetDialog parentDialog; public void setParentDialog(BottomSheetDialog parentDialog) { @@ -88,7 +81,7 @@ public void handleClick(String action, int actionId) { return ; } - String sdkKey = getMailchimpKey(); + String sdkKey = KeyProviderFactory.get().getMailchimpKey(); try { KeyboardUtils.hideKeyboard(this); diff --git a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java index cc806e680e..41ea6f18bb 100644 --- a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java +++ b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java @@ -3,10 +3,10 @@ import static android.os.VibrationEffect.DEFAULT_AMPLITUDE; import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.GNOSIS_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.OPTIMISTIC_MAIN_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.XDAI_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.POLYGON_ID; import android.annotation.SuppressLint; import android.content.Context; @@ -68,7 +68,7 @@ public class FunctionButtonBar extends LinearLayout implements AdapterView.OnIte private final Semaphore functionMapComplete = new Semaphore(1); private Map functions; private NonFungibleAdapterInterface adapter; - private List selection = new ArrayList<>(); + private final List selection = new ArrayList<>(); private StandardFunctionInterface callStandardFunctions; private BuyCryptoInterface buyFunctionInterface; private int buttonCount; @@ -162,7 +162,8 @@ public void setupFunctions(StandardFunctionInterface functionInterface, AssetDef { callStandardFunctions = functionInterface; adapter = adp; - selection = tokenIds; + selection.clear(); + if (tokenIds != null) selection.addAll(tokenIds); this.token = token; functions = assetSvs.getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); assetService = assetSvs; @@ -293,8 +294,7 @@ private void handleUseClick(ItemClick function) } else { - List selected = selection; - if (adapter != null) selected = adapter.getSelectedTokenIds(selection); + List selected = getSelectionFromAdapter(); callStandardFunctions.handleTokenScriptFunction(function.buttonText, selected); } } @@ -302,8 +302,7 @@ private void handleUseClick(ItemClick function) private boolean isSelectionValid(int buttonId) { - List selected = selection; - if (adapter != null) selected = adapter.getSelectedTokenIds(selection); + List selected = getSelectionFromAdapter(); if (token == null || token.checkSelectionValidity(selected)) { return true; @@ -315,6 +314,18 @@ private boolean isSelectionValid(int buttonId) } } + private List getSelectionFromAdapter() + { + if (adapter != null) + { + return adapter.getSelectedTokenIds(selection); + } + else + { + return selection; + } + } + private boolean hasCorrectTokens(TSAction action) { //get selected tokens @@ -346,11 +357,6 @@ public void onTokenClick(View view, Token token, List tokenIds, bool { int maxSelect = 1; - if (!selected && tokenIds.containsAll(selection)) - { - selection = new ArrayList<>(); - } - if (!selected) return; if (functions != null) @@ -370,7 +376,8 @@ public void onTokenClick(View view, Token token, List tokenIds, bool if (maxSelect <= 1) { - selection = tokenIds; + selection.clear(); + selection.addAll(tokenIds); if (adapter != null) adapter.setRadioButtons(true); } } @@ -381,7 +388,8 @@ public void onLongTokenClick(View view, Token token, List tokenIds) //show radio buttons of all token groups if (adapter != null) adapter.setRadioButtons(true); - selection = tokenIds; + selection.clear(); + selection.addAll(tokenIds); Vibrator vb = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (vb != null && vb.hasVibrator()) { @@ -621,7 +629,7 @@ private void addTokenScriptFunctions(Map availableFunctions, T */ private boolean setupCustomTokenActions() { - if (token.tokenInfo.chainId == MATIC_ID && token.isNonFungible()) + if (token.tokenInfo.chainId == POLYGON_ID && token.isNonFungible()) { return false; } @@ -652,7 +660,7 @@ else if (token.tokenInfo.chainId == BINANCE_MAIN_ID return true; } } - else if (token.tokenInfo.chainId == MATIC_ID) + else if (token.tokenInfo.chainId == POLYGON_ID) { addFunction(R.string.swap_with_quickswap); return true; @@ -665,13 +673,16 @@ private void getFunctionMap(AssetDefinitionService assetSvs) try { functionMapComplete.acquire(); - } catch (InterruptedException e) + } + catch (InterruptedException e) { Timber.e(e); } + findViewById(R.id.wait_buttons).setVisibility(View.VISIBLE); + //get the available map for this collection - assetSvs.fetchFunctionMap(token) + assetSvs.fetchFunctionMap(token, selection) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(availabilityMap -> setupTokenMap(token, availabilityMap), this::onMapFetchError) @@ -728,7 +739,7 @@ public void setupBuyFunction(BuyCryptoInterface buyCryptoInterface, OnRampReposi private void addBuyFunction() { if (token.tokenInfo.chainId == MAINNET_ID - || token.tokenInfo.chainId == XDAI_ID) + || token.tokenInfo.chainId == GNOSIS_ID) { addPurchaseVerb(token, onRampRepository); } diff --git a/app/src/main/java/com/alphawallet/app/widget/GasWidget.java b/app/src/main/java/com/alphawallet/app/widget/GasWidget.java index 17397680c4..230787019e 100644 --- a/app/src/main/java/com/alphawallet/app/widget/GasWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/GasWidget.java @@ -19,10 +19,12 @@ import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TXSpeed; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.repository.entity.RealmGasSpread; import com.alphawallet.app.repository.entity.RealmTokenTicker; +import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.alphawallet.app.service.GasService; import com.alphawallet.app.service.TickerService; import com.alphawallet.app.service.TokensService; @@ -38,6 +40,7 @@ import io.realm.Realm; import io.realm.RealmQuery; +import timber.log.Timber; /** * Created by JB on 19/11/2020. @@ -318,9 +321,14 @@ public void run() { GasSpeed2 gs = gasSpread.getSelectedGasFee(currentGasSpeedIndex); + if (gs == null || gs.gasPrice == null || gs.gasPrice.maxFeePerGas == null) + { + return; + } + Token baseCurrency = tokensService.getTokenOrBase(token.tokenInfo.chainId, token.getWallet()); BigInteger networkFee = gs.gasPrice.maxFeePerGas.multiply(getUseGasLimit()); - String gasAmountInBase = BalanceUtils.getScaledValueScientific(new BigDecimal(networkFee), baseCurrency.tokenInfo.decimals); + String gasAmountInBase = BalanceUtils.getSlidingBaseValue(new BigDecimal(networkFee), baseCurrency.tokenInfo.decimals, GasSettingsActivity.GAS_PRECISION); if (gasAmountInBase.equals("0")) gasAmountInBase = "0.0001"; String displayStr = getContext().getString(R.string.gas_amount, gasAmountInBase, baseCurrency.getSymbol()); @@ -349,7 +357,7 @@ public void run() } catch (Exception e) { - // + Timber.w(e); } timeEstimate.setText(displayStr); speedText.setText(gs.speed); diff --git a/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java b/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java index df0fa94fba..372395621f 100644 --- a/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java +++ b/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java @@ -18,6 +18,7 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.TXSpeed; +import com.alphawallet.app.entity.analytics.ActionSheetMode; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.repository.entity.Realm1559Gas; @@ -293,7 +294,7 @@ public void run() Token baseCurrency = tokensService.getTokenOrBase(token.tokenInfo.chainId, token.getWallet()); BigInteger networkFee = gs.gasPrice.maxFeePerGas.multiply(getUseGasLimit()); - String gasAmountInBase = BalanceUtils.getScaledValueScientific(new BigDecimal(networkFee), baseCurrency.tokenInfo.decimals); + String gasAmountInBase = BalanceUtils.getSlidingBaseValue(new BigDecimal(networkFee), baseCurrency.tokenInfo.decimals, GasSettingsActivity.GAS_PRECISION); if (gasAmountInBase.equals("0")) gasAmountInBase = "0.0001"; String displayStr = context.getString(R.string.gas_amount, gasAmountInBase, baseCurrency.getSymbol()); diff --git a/app/src/main/java/com/alphawallet/app/widget/InputAddress.java b/app/src/main/java/com/alphawallet/app/widget/InputAddress.java index 69d1e39aed..ac74fbfd34 100644 --- a/app/src/main/java/com/alphawallet/app/widget/InputAddress.java +++ b/app/src/main/java/com/alphawallet/app/widget/InputAddress.java @@ -1,7 +1,6 @@ package com.alphawallet.app.widget; import static android.content.Context.CLIPBOARD_SERVICE; - import static com.alphawallet.app.entity.tokenscript.TokenscriptFunction.ZERO_ADDRESS; import android.app.Activity; @@ -14,22 +13,20 @@ import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; -import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.AutoCompleteTextView; import android.widget.ImageButton; -import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.ENSCallback; -import com.alphawallet.app.entity.EnsNodeNotSyncCallback; import com.alphawallet.app.entity.Wallet; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.entity.analytics.QrScanSource; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.adapter.AutoCompleteAddressAdapter; import com.alphawallet.app.ui.widget.entity.AddressReadyCallback; import com.alphawallet.app.ui.widget.entity.BoxStatus; @@ -57,6 +54,34 @@ public class InputAddress extends RelativeLayout implements ItemClickListener, E private final RelativeLayout boxLayout; private final TextView errorText; private final Context context; + private final ENSHandler ensHandler; + private final Pattern findAddress = Pattern.compile("^(\\s?)+(0x)([0-9a-fA-F]{40})(\\s?)+\\z"); + private final float standardTextSize; + private final View.OnClickListener pasteListener = new OnClickListener() + { + @Override + public void onClick(View view) + { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); + try + { + CharSequence textToPaste = clipboard.getPrimaryClip().getItemAt(0).getText(); + editText.append(textToPaste); + } + catch (Exception e) + { + Timber.e(e); + } + } + }; + private final View.OnClickListener clearListener = new OnClickListener() + { + @Override + public void onClick(View view) + { + editText.getText().clear(); + } + }; private int labelResId; private int hintRedId; private boolean noCam; @@ -64,12 +89,9 @@ public class InputAddress extends RelativeLayout implements ItemClickListener, E private String fullAddress; private String ensName; private boolean handleENS = false; - private final ENSHandler ensHandler; private AWalletAlertDialog dialog; private AddressReadyCallback addressReadyCallback = null; private long chainOverride; - private final Pattern findAddress = Pattern.compile("^(\\s?)+(0x)([0-9a-fA-F]{40})(\\s?)+\\z"); - private final float standardTextSize; public InputAddress(Context context, AttributeSet attrs) { @@ -158,20 +180,7 @@ private void setViews() editText.setHint(hintRedId); - //Paste - pasteItem.setOnClickListener(v -> { - //from clipboard - ClipboardManager clipboard = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - try - { - CharSequence textToPaste = clipboard.getPrimaryClip().getItemAt(0).getText(); - editText.append(textToPaste); - } - catch (Exception e) - { - Timber.e(e, e.getMessage()); - } - }); + pasteItem.setOnClickListener(pasteListener); if (noCam) { @@ -181,7 +190,8 @@ private void setViews() { //QR Scanner scanQrIcon.setOnClickListener(v -> { - Intent intent = new Intent(context, QRScanner.class); + Intent intent = new Intent(context, QRScannerActivity.class); + intent.putExtra(QrScanSource.KEY, QrScanSource.ADDRESS_TEXT_FIELD.getValue()); intent.putExtra(C.EXTRA_CHAIN_ID, chainOverride); ((Activity) context).startActivityForResult(intent, C.BARCODE_READER_REQUEST_CODE); }); @@ -522,9 +532,25 @@ public void afterTextChanged(Editable s) { ensHandler.checkAddress(); } + + if (TextUtils.isEmpty(s)) + { + pasteItem.setText(R.string.paste); + pasteItem.setOnClickListener(pasteListener); + } + else + { + pasteItem.setText(R.string.action_clear); + pasteItem.setOnClickListener(clearListener); + } } - public void setEnsNodeNotSyncCallback(EnsNodeNotSyncCallback callback) + public long getChain() + { + return chainOverride; + } + + /*public void setEnsNodeNotSyncCallback(EnsNodeNotSyncCallback callback) { Timber.d("setEnsNodeNotSyncCallback: "); ensHandler.setEnsNodeNotSyncCallback(callback); @@ -533,6 +559,6 @@ public void setEnsNodeNotSyncCallback(EnsNodeNotSyncCallback callback) public void setEnsHandlerNodeSyncFlag(boolean performSync) { ensHandler.performEnsSync = performSync; - } + }*/ } diff --git a/app/src/main/java/com/alphawallet/app/widget/InputView.java b/app/src/main/java/com/alphawallet/app/widget/InputView.java index 7ed64226fc..23c4d098e7 100644 --- a/app/src/main/java/com/alphawallet/app/widget/InputView.java +++ b/app/src/main/java/com/alphawallet/app/widget/InputView.java @@ -7,7 +7,6 @@ import android.content.res.TypedArray; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.inputmethod.EditorInfo; @@ -18,7 +17,8 @@ import android.widget.TextView; import com.alphawallet.app.C; -import com.alphawallet.app.ui.QRScanning.QRScanner; +import com.alphawallet.app.entity.analytics.QrScanSource; +import com.alphawallet.app.ui.QRScanning.QRScannerActivity; import com.alphawallet.app.ui.widget.entity.BoxStatus; import com.alphawallet.app.util.Utils; @@ -113,7 +113,8 @@ private void getAttrs(Context context, AttributeSet attrs) { if (!noCam) { scanQrIcon.setOnClickListener(v -> { - Intent intent = new Intent(context, QRScanner.class); + Intent intent = new Intent(context, QRScannerActivity.class); + intent.putExtra(QrScanSource.KEY, QrScanSource.ADDRESS_TEXT_FIELD.getValue()); ((Activity) context).startActivityForResult(intent, C.BARCODE_READER_REQUEST_CODE); }); } diff --git a/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java b/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java index c306619edd..be579411d2 100644 --- a/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java +++ b/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java @@ -3,19 +3,22 @@ import static com.alphawallet.app.util.Utils.loadFile; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.media.MediaPlayer; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Base64; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.webkit.MimeTypeMap; +import android.webkit.WebChromeClient; import android.webkit.WebView; import android.widget.ImageView; import android.widget.ProgressBar; @@ -24,6 +27,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Token; @@ -39,6 +43,11 @@ import com.bumptech.glide.request.target.Target; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import timber.log.Timber; /** * Created by JB on 30/05/2021. @@ -52,7 +61,10 @@ public class NFTImageView extends RelativeLayout private final RelativeLayout fallbackLayout; private final TokenIcon fallbackIcon; private final ProgressBar progressBar; + private final ImageView overlay; private final Handler handler = new Handler(Looper.getMainLooper()); + private MediaPlayer mediaPlayer; + /** * Prevent glide dumping log errors - it is expected that load will fail */ @@ -61,11 +73,18 @@ public class NFTImageView extends RelativeLayout @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { - //couldn't load using glide, fallback to webview - if (model != null) + //couldn't load using glide + String msg = e != null ? e.toString() : ""; + if (msg.contains(C.GLIDE_URL_INVALID)) //URL not valid: use the attribute name + { + handler.post(() -> { + progressBar.setVisibility(View.GONE); + fallbackLayout.setVisibility(View.VISIBLE); + }); + } + else if (model != null) //or fallback to webview if there was some other problem { - progressBar.setVisibility(View.GONE); - setWebView(model.toString()); + setWebView(model.toString(), ImageType.IMAGE); } return false; } @@ -93,9 +112,12 @@ public NFTImageView(Context context, @Nullable AttributeSet attrs) fallbackLayout = findViewById(R.id.layout_fallback); fallbackIcon = findViewById(R.id.icon_fallback); progressBar = findViewById(R.id.avatar_progress_spinner); + overlay = findViewById(R.id.overlay_rect); + mediaPlayer = null; webLayout.setVisibility(View.GONE); webView.setVisibility(View.GONE); + showProgress = false; if (loadRequest != null && loadRequest.isRunning()) { @@ -108,16 +130,27 @@ public NFTImageView(Context context, @Nullable AttributeSet attrs) public void setupTokenImageThumbnail(NFTAsset asset) { + fallbackIcon.setupFallbackTextIcon(asset.getName()); loadImage(asset.getThumbnail(), asset.getBackgroundColor(), 1); } public void setupTokenImage(NFTAsset asset) throws IllegalArgumentException { - if (shouldLoad(asset.getImage())) + String anim = asset.getAnimation(); + fallbackIcon.setupFallbackTextIcon(asset.getName()); + + if (anim != null && !isGlb(anim) && !isAudio(anim)) + { + if (!shouldLoad(anim)) return; + //attempt to load animation + setWebView(anim, ImageType.ANIM); + } + else if (shouldLoad(asset.getImage())) { - showLoadingProgress(true); + showLoadingProgress(); progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE); loadImage(asset.getImage(), asset.getBackgroundColor(), 16); + playAudioIfAvailable(anim); } } @@ -132,14 +165,15 @@ private void loadImage(String url, String backgroundColor, int corners) throws I image.setVisibility(View.VISIBLE); webLayout.setVisibility(View.GONE); - if (!TextUtils.isEmpty(backgroundColor)) + try { int color = Color.parseColor("#" + backgroundColor); ColorStateList sl = ColorStateList.valueOf(color); holdingView.setBackgroundTintList(sl); } - else + catch (Exception e) { + Timber.w(e); holdingView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.transparent)); } @@ -153,19 +187,54 @@ private void loadImage(String url, String backgroundColor, int corners) throws I .into(new DrawableImageViewTarget(image)).getRequest(); } - private void setWebView(String imageUrl) + @SuppressLint("SetJavaScriptEnabled") + private void setWebView(String imageUrl, ImageType hint) { - String loader = loadFile(getContext(), R.raw.token_graphic).replace("[URL]", imageUrl); - String base64 = android.util.Base64.encodeToString(loader.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); - webView.setVerticalScrollBarEnabled(false); webView.setHorizontalScrollBarEnabled(false); + webView.setWebChromeClient(new WebChromeClient()); + + //determine how to display this URL + final DisplayType useType = new DisplayType(imageUrl, hint); handler.post(() -> { + this.imageUrl = imageUrl; image.setVisibility(View.GONE); webLayout.setVisibility(View.VISIBLE); webView.setVisibility(View.VISIBLE); - webView.loadData(base64, "text/html; charset=utf-8", "base64"); + overlay.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + + if (useType.getImageType() == ImageType.WEB) + { + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); + webView.loadUrl(imageUrl); + } + else if (useType.getImageType() == ImageType.ANIM) + { + String loaderAnim = loadFile(getContext(), R.raw.token_anim).replace("[URL]", imageUrl).replace("[MIME]", useType.getMimeType()); + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); + webView.getSettings().setMediaPlaybackRequiresUserGesture(false); + webView.getSettings().setAllowContentAccess(true); + webView.getSettings().setBlockNetworkLoads(false); + webView.getSettings().setDomStorageEnabled(true); + String base64 = android.util.Base64.encodeToString(loaderAnim.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); + webView.loadData(base64, "text/html; charset=utf-8", "base64"); + } + else if (useType.getImageType() == ImageType.MODEL) + { + String loader = loadFile(getContext(), R.raw.token_model).replace("[URL]", imageUrl); + String base64 = android.util.Base64.encodeToString(loader.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); + webView.loadData(base64, "text/html; charset=utf-8", "base64"); + } + else + { + String loader = loadFile(getContext(), R.raw.token_graphic).replace("[URL]", imageUrl); + String base64 = android.util.Base64.encodeToString(loader.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); + webView.loadData(base64, "text/html; charset=utf-8", "base64"); + } }); } @@ -210,15 +279,9 @@ public boolean hasContent() return hasContent; } - public void showLoadingProgress(boolean showProgress) + public void showLoadingProgress() { - this.showProgress = showProgress; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) - { - return true; + this.showProgress = true; } public boolean shouldLoad(String url) @@ -249,4 +312,147 @@ public boolean isDisplayingImage() { return !TextUtils.isEmpty(imageUrl); } + + private boolean isGlb(String url) + { + return (url != null && MimeTypeMap.getFileExtensionFromUrl(url).equals("glb")); + } + + private static final List audioTypes = new ArrayList<>(Arrays.asList( "mp3", "ogg", "wav", "flac", "aac", "opus", "weba" )); + + private boolean isAudio(String url) + { + if (url == null) + { + return false; + } + else + { + return audioTypes.contains(MimeTypeMap.getFileExtensionFromUrl(url)); + } + } + + private void playAudioIfAvailable(String url) + { + if (!isAudio(url)) + { + return; + } + + //set up MediaPlayer + mediaPlayer = new MediaPlayer(); + + try + { + mediaPlayer.setDataSource(url); + mediaPlayer.prepare(); + mediaPlayer.start(); + mediaPlayer.setLooping(true); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void onDestroy() + { + if (mediaPlayer != null) + { + try + { + mediaPlayer.reset(); + mediaPlayer.release(); + mediaPlayer = null; + } + catch (Exception e) + { + Timber.w(e); + } + } + } + + public void onPause() + { + if (mediaPlayer != null && mediaPlayer.isPlaying()) + { + mediaPlayer.pause(); + } + } + + public void onResume() + { + if (mediaPlayer != null && !mediaPlayer.isPlaying()) + { + mediaPlayer.start(); + } + } + + private static class DisplayType + { + private final ImageType type; + private final String mimeStr; + + // Should handle most cases; this is a handler for anim or drop through cases, + // Previously these were not handled so this is a big improvement in display handling + public DisplayType(String url, ImageType hint) + { + if (url == null || url.length() < 5) + { + type = hint; + mimeStr = ""; + return; + } + + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + + switch (extension) + { + case "": + mimeStr = ""; + if (hint == ImageType.IMAGE) + { + type = hint; + } + else + { + type = ImageType.WEB; + } + break; + case "mp4": + case "webm": + case "avi": + case "mpeg": + case "mpg": + case "m2v": + type = ImageType.ANIM; + mimeStr = "video/" + extension; + break; + case "bmp": + case "png": + case "jpg": + case "svg": + case "glb": //currently avoid handling these + default: + type = ImageType.IMAGE; + mimeStr = "image/" + extension; + break; + } + } + + public String getMimeType() + { + return mimeStr; + } + + public ImageType getImageType() + { + return type; + } + } + + private enum ImageType + { + IMAGE, ANIM, WEB, MODEL, AUDIO + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/SelectTokenDialog.java b/app/src/main/java/com/alphawallet/app/widget/SelectTokenDialog.java index d567e5b4f4..bc2ce27ba8 100644 --- a/app/src/main/java/com/alphawallet/app/widget/SelectTokenDialog.java +++ b/app/src/main/java/com/alphawallet/app/widget/SelectTokenDialog.java @@ -19,9 +19,8 @@ import androidx.recyclerview.widget.RecyclerView; import com.alphawallet.app.R; -import com.alphawallet.app.entity.lifi.Connection; +import com.alphawallet.app.entity.lifi.Token; import com.alphawallet.app.ui.widget.adapter.SelectTokenAdapter; -import com.alphawallet.app.ui.widget.divider.ListDivider; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -32,7 +31,6 @@ public class SelectTokenDialog extends BottomSheetDialog private final Handler handler = new Handler(Looper.getMainLooper()); private RecyclerView tokenList; private SelectTokenAdapter adapter; - private List tokenItems; private LinearLayout searchLayout; private EditText search; private TextView noResultsText; @@ -58,10 +56,9 @@ public SelectTokenDialog(@NonNull Activity activity) btnClose.setOnClickListener(v -> dismiss()); } - public SelectTokenDialog(List tokenItems, Activity activity, SelectTokenDialogEventListener callback) + public SelectTokenDialog(List tokenItems, Activity activity, SelectTokenDialogEventListener callback) { this(activity); - this.tokenItems = tokenItems; noResultsText.setVisibility(tokenItems.size() > 0 ? View.GONE : View.VISIBLE); @@ -105,6 +102,6 @@ public void setSelectedToken(String address) public interface SelectTokenDialogEventListener { - void onChainSelected(Connection.LToken tokenItem); + void onChainSelected(Token tokenItem); } } diff --git a/app/src/main/java/com/alphawallet/app/widget/SignDataWidget.java b/app/src/main/java/com/alphawallet/app/widget/SignDataWidget.java index 34d247aa2d..99d1b55792 100644 --- a/app/src/main/java/com/alphawallet/app/widget/SignDataWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/SignDataWidget.java @@ -1,6 +1,7 @@ package com.alphawallet.app.widget; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; @@ -12,6 +13,8 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.ActionSheetInterface; +import com.alphawallet.app.util.Hex; +import com.alphawallet.token.entity.SignMessageType; import com.alphawallet.token.entity.Signable; /** @@ -19,12 +22,11 @@ */ public class SignDataWidget extends LinearLayout { - private final TextView textSignDetails; - private final TextView textSignDetailsMax; + private final TextView previewText; + private final TextView messageText; private final LinearLayout layoutHolder; private final ImageView moreArrow; private final ScrollView scrollView; - private final TextView messageTitle; private ActionSheetInterface sheetInterface; private Signable signable; @@ -32,34 +34,52 @@ public SignDataWidget(Context context, @Nullable AttributeSet attrs) { super(context, attrs); inflate(context, R.layout.item_sign_data, this); - textSignDetails = findViewById(R.id.text_sign_data); - textSignDetailsMax = findViewById(R.id.text_sign_data_max); + previewText = findViewById(R.id.text_preview); + messageText = findViewById(R.id.text_message); layoutHolder = findViewById(R.id.layout_holder); moreArrow = findViewById(R.id.image_more); scrollView = findViewById(R.id.scroll_view); - messageTitle = findViewById(R.id.text_message_title); + TextView messageTitle = findViewById(R.id.text_message_title); + boolean noTitle = getAttribute(context, attrs); + if (noTitle) + { + messageTitle.setText(""); + messageTitle.setVisibility(GONE); + } } - public void setupSignData(Signable message) + private boolean getAttribute(Context context, AttributeSet attrs) { - this.signable = message; - textSignDetails.setText(message.getUserMessage()); - textSignDetailsMax.setText(message.getUserMessage()); + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.SignDataWidget, + 0, 0 + ); + + return a.getBoolean(R.styleable.SignDataWidget_noTitle, false); + } + + public void setupSignData(Signable signable) + { + this.signable = signable; + String message = signable.getUserMessage().toString(); + previewText.setText(message); + messageText.setText(message); layoutHolder.setOnClickListener(v -> { - if (textSignDetails.getVisibility() == View.VISIBLE) + if (previewText.getVisibility() == View.VISIBLE) { - textSignDetails.setVisibility(View.GONE); + previewText.setVisibility(View.INVISIBLE); scrollView.setVisibility(View.VISIBLE); - messageTitle.setVisibility(View.GONE); + scrollView.setEnabled(true); moreArrow.setImageResource(R.drawable.ic_expand_less_black); if (sheetInterface != null) sheetInterface.lockDragging(true); } else { - textSignDetails.setVisibility(View.VISIBLE); - messageTitle.setVisibility(View.VISIBLE); + previewText.setVisibility(View.VISIBLE); scrollView.setVisibility(View.GONE); + scrollView.setEnabled(false); moreArrow.setImageResource(R.drawable.ic_expand_more); if (sheetInterface != null) sheetInterface.lockDragging(false); } @@ -75,4 +95,4 @@ public Signable getSignable() { return signable; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/widget/SignTransactionDialog.java b/app/src/main/java/com/alphawallet/app/widget/SignTransactionDialog.java index f984d99b33..3f60a5c579 100644 --- a/app/src/main/java/com/alphawallet/app/widget/SignTransactionDialog.java +++ b/app/src/main/java/com/alphawallet/app/widget/SignTransactionDialog.java @@ -22,6 +22,7 @@ import com.alphawallet.app.entity.AuthenticationCallback; import com.alphawallet.app.entity.AuthenticationFailType; import com.alphawallet.app.entity.Operation; +import com.alphawallet.app.ui.BaseActivity; import java.security.ProviderException; import java.util.concurrent.Executor; @@ -174,8 +175,16 @@ private void showAuthenticationScreen(Activity activity, AuthenticationCallback else if (km != null) { Intent intent = km.createConfirmDeviceCredentialIntent(activity.getString(R.string.unlock_private_key), ""); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - activity.startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + callBackId.ordinal()); + if (intent == null) + { + authCallback.authenticateFail("Can not unlock", AuthenticationFailType.BIOMETRIC_AUTHENTICATION_NOT_AVAILABLE, callBackId); + } + else + { + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + BaseActivity.authCallback = authCallback; + activity.startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS + callBackId.ordinal()); + } } else { @@ -197,4 +206,4 @@ public void close() } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/widget/StandardHeader.java b/app/src/main/java/com/alphawallet/app/widget/StandardHeader.java index 0eb01a7490..2cd0a27315 100644 --- a/app/src/main/java/com/alphawallet/app/widget/StandardHeader.java +++ b/app/src/main/java/com/alphawallet/app/widget/StandardHeader.java @@ -4,6 +4,7 @@ import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -16,6 +17,8 @@ public class StandardHeader extends LinearLayout { private TextView headerText; + private TextView textControl; + private ImageView imageControl; private ChainName chainName; private SwitchMaterial switchMaterial; private View separator; @@ -40,30 +43,41 @@ private void getAttrs(Context context, AttributeSet attrs) int headerId = a.getResourceId(R.styleable.StandardHeader_headerText, R.string.empty); boolean showSwitch = a.getBoolean(R.styleable.StandardHeader_showSwitch, false); boolean showChainName = a.getBoolean(R.styleable.StandardHeader_showChain, false); + boolean showTextControl = a.getBoolean(R.styleable.StandardHeader_showTextControl, false); + boolean showImageControl = a.getBoolean(R.styleable.StandardHeader_showImageControl, false); + int controlText = a.getResourceId(R.styleable.StandardHeader_controlText, -1); + int controlImageRes = a.getResourceId(R.styleable.StandardHeader_controlImageRes, -1); headerText = findViewById(R.id.text_header); chainName = findViewById(R.id.chain_name); switchMaterial = findViewById(R.id.switch_material); separator = findViewById(R.id.separator); + textControl = findViewById(R.id.text_control); + imageControl = findViewById(R.id.image_control); headerText.setText(headerId); - if (showSwitch) + switchMaterial.setVisibility(showSwitch ? View.VISIBLE : View.GONE); + chainName.setVisibility(showChainName ? View.VISIBLE : View.GONE); + + if (showTextControl) { - switchMaterial.setVisibility(View.VISIBLE); + textControl.setVisibility(View.VISIBLE); + textControl.setText(controlText); } else { - switchMaterial.setVisibility(View.GONE); + textControl.setVisibility(View.GONE); } - if (showChainName) + if (showImageControl) { - chainName.setVisibility(View.VISIBLE); + imageControl.setVisibility(View.VISIBLE); + imageControl.setImageResource(controlImageRes); } else { - chainName.setVisibility(View.GONE); + imageControl.setVisibility(View.GONE); } } finally @@ -92,6 +106,16 @@ public SwitchMaterial getSwitch() return switchMaterial; } + public TextView getTextControl() + { + return textControl; + } + + public ImageView getImageControl() + { + return imageControl; + } + public void hideSeparator() { separator.setVisibility(View.GONE); diff --git a/app/src/main/java/com/alphawallet/app/widget/SwapSettingsDialog.java b/app/src/main/java/com/alphawallet/app/widget/SwapSettingsDialog.java index f47edf97f0..1ae6da13e5 100644 --- a/app/src/main/java/com/alphawallet/app/widget/SwapSettingsDialog.java +++ b/app/src/main/java/com/alphawallet/app/widget/SwapSettingsDialog.java @@ -3,10 +3,11 @@ import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; import android.app.Activity; -import android.content.DialogInterface; +import android.content.Intent; import android.content.res.Resources; import android.view.View; import android.widget.ImageView; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; @@ -14,19 +15,25 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.lifi.Chain; +import com.alphawallet.app.entity.lifi.SwapProvider; +import com.alphawallet.app.ui.SelectSwapProvidersActivity; +import com.alphawallet.app.ui.widget.adapter.ChainFilter; import com.alphawallet.app.ui.widget.adapter.SelectChainAdapter; -import com.alphawallet.app.ui.widget.divider.ListDivider; +import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; import java.util.List; +import java.util.Set; public class SwapSettingsDialog extends BottomSheetDialog { private RecyclerView chainList; private SelectChainAdapter adapter; - private List chains; + private List swapProviders; private SlippageWidget slippageWidget; + private StandardHeader preferredExchangesHeader; + private FlexboxLayout preferredSwapProviders; public SwapSettingsDialog(@NonNull Activity activity) { @@ -36,7 +43,7 @@ public SwapSettingsDialog(@NonNull Activity activity) setOnShowListener(dialogInterface -> { view.setMinimumHeight(Resources.getSystem().getDisplayMetrics().heightPixels); - BottomSheetBehaviorbehavior = BottomSheetBehavior.from((View) view.getParent()); + BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent()); behavior.setState(STATE_EXPANDED); behavior.setSkipCollapsed(true); }); @@ -46,15 +53,55 @@ public SwapSettingsDialog(@NonNull Activity activity) ImageView closeBtn = findViewById(R.id.image_close); closeBtn.setOnClickListener(v -> dismiss()); + + preferredExchangesHeader = findViewById(R.id.header_exchanges); + preferredExchangesHeader.getTextControl().setOnClickListener(v -> { + Intent intent = new Intent(activity, SelectSwapProvidersActivity.class); + activity.startActivity(intent); + }); + + preferredSwapProviders = findViewById(R.id.layout_exchanges); } - public SwapSettingsDialog(Activity activity, List chains, SwapSettingsInterface swapSettingsInterface) + public SwapSettingsDialog(Activity activity, + List chains, + List swapProviders, + Set preferredSwapProviders, + SwapSettingsInterface swapSettingsInterface) { this(activity); - - adapter = new SelectChainAdapter(activity, chains, swapSettingsInterface); + ChainFilter filter = new ChainFilter(chains); + adapter = new SelectChainAdapter(activity, filter.getSupportedChains(), swapSettingsInterface); chainList.setLayoutManager(new LinearLayoutManager(getContext())); chainList.setAdapter(adapter); + this.swapProviders = swapProviders; + setSwapProviders(preferredSwapProviders); + } + + private TextView createTextView(String name) + { + int margin = (int) getContext().getResources().getDimension(R.dimen.tiny_8); + FlexboxLayout.LayoutParams params = + new FlexboxLayout.LayoutParams(FlexboxLayout.LayoutParams.WRAP_CONTENT, FlexboxLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(margin, margin, margin, margin); + + TextView exchange = new TextView(getContext(), null); + exchange.setText(name); + exchange.setLayoutParams(params); + return exchange; + } + + public void setSwapProviders(Set swapProviders) + { + preferredSwapProviders.removeAllViews(); + for (SwapProvider provider : this.swapProviders) + { + if (swapProviders.contains(provider.key)) + { + preferredSwapProviders.addView(createTextView(provider.name)); + } + } + preferredSwapProviders.invalidate(); } public void setChains(List chains) diff --git a/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java b/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java index 1d1fb5759a..74c343f39e 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java +++ b/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java @@ -2,8 +2,12 @@ import static androidx.core.content.ContextCompat.getColorStateList; +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; + import android.content.Context; import android.content.res.TypedArray; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; @@ -315,6 +319,13 @@ private void setupTextIcon(@NotNull Token token) } } + public void setupFallbackTextIcon(String name) + { + textIcon.setText(name); + textIcon.setVisibility(View.VISIBLE); + textIcon.setBackgroundTintList(getColorStateList(getContext(), EthereumNetworkBase.getChainColour(MAINNET_ID))); + } + public void setOnTokenClickListener(TokensAdapterCallback tokensAdapterCallback) { this.tokensAdapterCallback = tokensAdapterCallback; @@ -353,10 +364,6 @@ public boolean onResourceReady(Drawable resource, Object model, Target public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { if (model == null || token == null || !model.toString().toLowerCase().contains(token.getAddress())) return false; - if (token != null) - { - IconItem.noIconFound(token.tokenInfo.chainId, token.getAddress()); //don't try to load this asset again for this session - } return false; } @@ -398,4 +405,21 @@ private void loadImageFromResource(int resourceId) icon.setVisibility(View.VISIBLE); findViewById(R.id.circle).setVisibility(View.VISIBLE); } + + public void setGrayscale(boolean grayscale) + { + if (grayscale) + { + ColorMatrix matrix = new ColorMatrix(); + matrix.setSaturation(0); + ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix); + icon.setColorFilter(cf); + icon.setImageAlpha(128); + } + else + { + icon.setColorFilter(null); + icon.setImageAlpha(255); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/TokenInfoView.java b/app/src/main/java/com/alphawallet/app/widget/TokenInfoView.java index 956f177321..4d13f0149f 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TokenInfoView.java +++ b/app/src/main/java/com/alphawallet/app/widget/TokenInfoView.java @@ -1,5 +1,9 @@ package com.alphawallet.app.widget; +import static android.content.Context.CLIPBOARD_SERVICE; + +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; @@ -9,11 +13,13 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import androidx.core.content.ContextCompat; import com.alphawallet.app.R; import com.alphawallet.app.service.TickerService; +import com.alphawallet.app.util.Utils; public class TokenInfoView extends LinearLayout { @@ -79,6 +85,33 @@ public void setValue(String text) } } + public void setCopyableValue(String text) + { + if (!TextUtils.isEmpty(text)) + { + setVisibility(View.VISIBLE); + String display = text; + // If text is an instance of an address, format it; otherwise do nothing + if (Utils.isAddressValid(text)) + { + display = Utils.formatAddress(text); + } + TextView useView = getTextView(display.length()); + useView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_copy, 0); + useView.setText(display); + setCopyListener(useView, label.getText(), text); + } + } + + private void setCopyListener(TextView textView, CharSequence clipLabel, CharSequence clipValue) + { + textView.setOnClickListener(view -> { + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText(clipLabel, clipValue)); + Toast.makeText(getContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); + }); + } + public void setCurrencyValue(double v) { setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/alphawallet/app/widget/TokenSelector.java b/app/src/main/java/com/alphawallet/app/widget/TokenSelector.java index cf22edfb4b..4c74fb4ec2 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TokenSelector.java +++ b/app/src/main/java/com/alphawallet/app/widget/TokenSelector.java @@ -10,19 +10,16 @@ import android.util.AttributeSet; import android.view.View; import android.widget.EditText; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.core.content.ContextCompat; import com.alphawallet.app.R; -import com.alphawallet.app.entity.lifi.Connection; +import com.alphawallet.app.entity.lifi.Token; import com.alphawallet.app.util.Utils; -import com.bumptech.glide.Glide; import com.google.android.material.button.MaterialButton; - public class TokenSelector extends LinearLayout { private final Handler handler = new Handler(Looper.getMainLooper()); @@ -38,7 +35,7 @@ public class TokenSelector extends LinearLayout private final TextView error; private Runnable runnable; private TokenSelectorEventListener callback; - private Connection.LToken tokenItem; + private Token tokenItem; public TokenSelector(Context context, AttributeSet attrs) { @@ -146,7 +143,7 @@ public void reset() setVisibility(View.VISIBLE); } - public void init(Connection.LToken tokenItem) + public void init(Token tokenItem) { this.tokenItem = tokenItem; @@ -192,7 +189,7 @@ public void afterTextChanged(Editable editable) }); } - public Connection.LToken getToken() + public Token getToken() { return this.tokenItem; } @@ -207,6 +204,11 @@ public void setAmount(String amount) editText.setText(amount); } + public void clearAmount() + { + editText.getText().clear(); + } + public void setBalance(String amount) { StringBuilder balanceStr = new StringBuilder(getContext().getString(R.string.label_balance)); @@ -253,7 +255,7 @@ public interface TokenSelectorEventListener /** * Triggered when a new Token is selected. **/ - void onSelectionChanged(Connection.LToken token); + void onSelectionChanged(Token token); /** * Triggered when the `Max` button is clicked. diff --git a/app/src/main/java/com/alphawallet/app/widget/TokensBalanceView.java b/app/src/main/java/com/alphawallet/app/widget/TokensBalanceView.java new file mode 100644 index 0000000000..110666343e --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/widget/TokensBalanceView.java @@ -0,0 +1,36 @@ +package com.alphawallet.app.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import com.alphawallet.app.R; +import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.ui.widget.adapter.TestNetHorizontalListAdapter; + + +public class TokensBalanceView extends LinearLayout +{ + RecyclerView horizontalListView; + + public TokensBalanceView(Context context, @Nullable AttributeSet attrs) + { + super(context, attrs); + inflate(context, R.layout.item_token_with_balance_view, this); + horizontalListView = findViewById(R.id.horizontal_list); + } + + public void bindTokens(Token[] token) + { + TestNetHorizontalListAdapter testNetHorizontalListAdapter = new TestNetHorizontalListAdapter(token, getContext()); + horizontalListView.setAdapter(testNetHorizontalListAdapter); + } + + public void blankView() + { + //clear adapter + bindTokens(new Token[0]); + } +} + diff --git a/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java b/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java index c1fa06df23..6def42d43b 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java @@ -55,7 +55,7 @@ public void setupTransaction(Web3Transaction w3tx, long chainId, String walletAd else { TransactionInput transactionInput = Transaction.decoder.decodeInput(w3tx, chainId, walletAddress); - textTransactionSummary.setText(transactionInput.getOperationTitle(getContext())); + textTransactionSummary.setText(transactionInput.buildFunctionCallText()); } layoutHolder.setOnClickListener(v -> { diff --git a/app/src/main/java/com/alphawallet/app/widget/UserAvatar.java b/app/src/main/java/com/alphawallet/app/widget/UserAvatar.java index 62c93b1a7a..34cee27d78 100644 --- a/app/src/main/java/com/alphawallet/app/widget/UserAvatar.java +++ b/app/src/main/java/com/alphawallet/app/widget/UserAvatar.java @@ -18,12 +18,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.R; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.repository.TokenRepository; import com.alphawallet.app.ui.widget.entity.AvatarWriteCallback; -import com.alphawallet.app.util.AWEnsResolver; +import com.alphawallet.app.util.ens.AWEnsResolver; import com.alphawallet.app.util.Blockies; import com.bumptech.glide.Glide; import com.bumptech.glide.load.DataSource; diff --git a/app/src/main/res/anim/hold.xml b/app/src/main/res/anim/hold.xml new file mode 100644 index 0000000000..3df8c1db32 --- /dev/null +++ b/app/src/main/res/anim/hold.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000000..e5a91703ea --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000000..c0f406cb01 --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/icons_illustrations_analytics.png b/app/src/main/res/drawable-hdpi/icons_illustrations_analytics.png new file mode 100644 index 0000000000..7ad451bb66 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icons_illustrations_analytics.png differ diff --git a/app/src/main/res/drawable-mdpi/icons_illustrations_analytics.png b/app/src/main/res/drawable-mdpi/icons_illustrations_analytics.png new file mode 100644 index 0000000000..1518fe63c5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/icons_illustrations_analytics.png differ diff --git a/app/src/main/res/drawable-xhdpi/icons_illustrations_analytics.png b/app/src/main/res/drawable-xhdpi/icons_illustrations_analytics.png new file mode 100644 index 0000000000..9b2021fb7e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/icons_illustrations_analytics.png differ diff --git a/app/src/main/res/drawable-xxhdpi/icons_illustrations_analytics.png b/app/src/main/res/drawable-xxhdpi/icons_illustrations_analytics.png new file mode 100644 index 0000000000..5286162555 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icons_illustrations_analytics.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/icons_illustrations_analytics.png b/app/src/main/res/drawable-xxxhdpi/icons_illustrations_analytics.png new file mode 100644 index 0000000000..cb885160bd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/icons_illustrations_analytics.png differ diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000000..53ba4d7726 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_flags_indonesia.xml b/app/src/main/res/drawable/ic_flags_indonesia.xml new file mode 100644 index 0000000000..c118a21341 --- /dev/null +++ b/app/src/main/res/drawable/ic_flags_indonesia.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icons_tokens_sokol.xml b/app/src/main/res/drawable/ic_icons_tokens_sokol.xml deleted file mode 100644 index 666195478c..0000000000 --- a/app/src/main/res/drawable/ic_icons_tokens_sokol.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_key_status.xml b/app/src/main/res/drawable/ic_key_status.xml new file mode 100644 index 0000000000..1339fb3b8b --- /dev/null +++ b/app/src/main/res/drawable/ic_key_status.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_kovan.xml b/app/src/main/res/drawable/ic_kovan.xml deleted file mode 100644 index e68cb4f221..0000000000 --- a/app/src/main/res/drawable/ic_kovan.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_phi_network.png b/app/src/main/res/drawable/ic_phi_network.png deleted file mode 100644 index 931b13fcb5..0000000000 Binary files a/app/src/main/res/drawable/ic_phi_network.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_rinkeby.xml b/app/src/main/res/drawable/ic_rinkeby.xml deleted file mode 100644 index 9ce4a35d58..0000000000 --- a/app/src/main/res/drawable/ic_rinkeby.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_ropsten.xml b/app/src/main/res/drawable/ic_ropsten.xml deleted file mode 100644 index 0fb5615cc6..0000000000 --- a/app/src/main/res/drawable/ic_ropsten.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_sepolia_test.png b/app/src/main/res/drawable/ic_sepolia_test.png new file mode 100644 index 0000000000..1274b0f928 Binary files /dev/null and b/app/src/main/res/drawable/ic_sepolia_test.png differ diff --git a/app/src/main/res/drawable/ic_settings_analytics.xml b/app/src/main/res/drawable/ic_settings_analytics.xml new file mode 100644 index 0000000000..1be06e5619 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_analytics.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_settings_crash_reporting.xml b/app/src/main/res/drawable/ic_settings_crash_reporting.xml new file mode 100644 index 0000000000..f2858f05f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_crash_reporting.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_swap_horizontal.xml b/app/src/main/res/drawable/ic_swap_horizontal.xml new file mode 100644 index 0000000000..5550dac617 --- /dev/null +++ b/app/src/main/res/drawable/ic_swap_horizontal.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_wallet_connect_card.xml b/app/src/main/res/drawable/ic_wallet_connect_card.xml new file mode 100644 index 0000000000..079e981fc2 --- /dev/null +++ b/app/src/main/res/drawable/ic_wallet_connect_card.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/masking_circle.xml b/app/src/main/res/drawable/masking_circle.xml index 2656d493c3..04ab999632 100644 --- a/app/src/main/res/drawable/masking_circle.xml +++ b/app/src/main/res/drawable/masking_circle.xml @@ -1,12 +1,5 @@ - - \ No newline at end of file + android:shape="oval"> + + diff --git a/app/src/main/res/drawable/masking_rectangle.xml b/app/src/main/res/drawable/masking_rectangle.xml new file mode 100644 index 0000000000..d5db0da32d --- /dev/null +++ b/app/src/main/res/drawable/masking_rectangle.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_bar_spinner2.xml b/app/src/main/res/drawable/progress_bar_spinner2.xml new file mode 100644 index 0000000000..944108bffc --- /dev/null +++ b/app/src/main/res/drawable/progress_bar_spinner2.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/select_masking_circle.xml b/app/src/main/res/drawable/select_masking_circle.xml index 11731c2d51..7e8004539e 100644 --- a/app/src/main/res/drawable/select_masking_circle.xml +++ b/app/src/main/res/drawable/select_masking_circle.xml @@ -4,9 +4,5 @@ android:shape="ring" android:thicknessRatio="1" android:useLevel="false"> - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_add_custom_rpc_network.xml b/app/src/main/res/layout/activity_add_custom_rpc_network.xml index b229c8bfc2..b6c35d629a 100644 --- a/app/src/main/res/layout/activity_add_custom_rpc_network.xml +++ b/app/src/main/res/layout/activity_add_custom_rpc_network.xml @@ -8,6 +8,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_coinbase_pay.xml b/app/src/main/res/layout/activity_coinbase_pay.xml new file mode 100644 index 0000000000..f87d7f0655 --- /dev/null +++ b/app/src/main/res/layout/activity_coinbase_pay.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_crash_report_settings.xml b/app/src/main/res/layout/activity_crash_report_settings.xml new file mode 100644 index 0000000000..65393b8e70 --- /dev/null +++ b/app/src/main/res/layout/activity_crash_report_settings.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 7206a7004f..db9b164d78 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -1,5 +1,6 @@ diff --git a/app/src/main/res/layout/activity_my_address.xml b/app/src/main/res/layout/activity_my_address.xml index 7f92a97420..835a1c2dbc 100644 --- a/app/src/main/res/layout/activity_my_address.xml +++ b/app/src/main/res/layout/activity_my_address.xml @@ -78,7 +78,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" - custom:bold="true" /> + custom:lines="2" + custom:bold="true"/> diff --git a/app/src/main/res/layout/activity_nft_asset_detail.xml b/app/src/main/res/layout/activity_nft_asset_detail.xml index c94456e42b..d391476872 100644 --- a/app/src/main/res/layout/activity_nft_asset_detail.xml +++ b/app/src/main/res/layout/activity_nft_asset_detail.xml @@ -119,6 +119,13 @@ android:visibility="gone" custom:tokenInfoLabel="@string/asset_total_supply" /> + + + + diff --git a/app/src/main/res/layout/activity_select_route.xml b/app/src/main/res/layout/activity_select_route.xml new file mode 100644 index 0000000000..2b42773762 --- /dev/null +++ b/app/src/main/res/layout/activity_select_route.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_swap.xml b/app/src/main/res/layout/activity_swap.xml index 440bc7acf9..266a09e616 100644 --- a/app/src/main/res/layout/activity_swap.xml +++ b/app/src/main/res/layout/activity_swap.xml @@ -13,135 +13,181 @@ style="@style/Aw.Component.Separator" android:layout_below="@id/toolbar" /> - - - - - - - - - + android:layout_above="@id/btn_continue" + android:layout_below="@id/separator"> - - - - - - - - - - - - - - - - - - - - - - - - + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_transaction_detail.xml b/app/src/main/res/layout/activity_transaction_detail.xml index 4f44f371d2..963af1df85 100644 --- a/app/src/main/res/layout/activity_transaction_detail.xml +++ b/app/src/main/res/layout/activity_transaction_detail.xml @@ -359,11 +359,11 @@ android:id="@+id/layout_1559" android:layout_width="match_parent" android:layout_height="wrap_content" - android:baselineAligned="false" android:layout_marginTop="6dp" + android:baselineAligned="false" + android:orientation="horizontal" android:visibility="gone" - tools:visibility="visible" - android:orientation="horizontal"> + tools:visibility="visible"> @@ -422,9 +422,9 @@ tools:text="1.2" /> diff --git a/app/src/main/res/layout/activity_wallet_connect.xml b/app/src/main/res/layout/activity_wallet_connect.xml index d55c4b534d..50e756d883 100644 --- a/app/src/main/res/layout/activity_wallet_connect.xml +++ b/app/src/main/res/layout/activity_wallet_connect.xml @@ -190,6 +190,37 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_wallet_connect_v2.xml b/app/src/main/res/layout/activity_wallet_connect_v2.xml new file mode 100644 index 0000000000..4ede5e5edb --- /dev/null +++ b/app/src/main/res/layout/activity_wallet_connect_v2.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_wallet_diagnostic.xml b/app/src/main/res/layout/activity_wallet_diagnostic.xml new file mode 100644 index 0000000000..b668d0b21a --- /dev/null +++ b/app/src/main/res/layout/activity_wallet_diagnostic.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_action_sheet_message.xml b/app/src/main/res/layout/dialog_action_sheet_message.xml deleted file mode 100644 index 95c2912bed..0000000000 --- a/app/src/main/res/layout/dialog_action_sheet_message.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_action_sheet_sign.xml b/app/src/main/res/layout/dialog_action_sheet_sign.xml index 549eb08bb8..4d1b5c844e 100644 --- a/app/src/main/res/layout/dialog_action_sheet_sign.xml +++ b/app/src/main/res/layout/dialog_action_sheet_sign.xml @@ -17,6 +17,13 @@ android:layout_height="wrap_content" custom:label="@string/requester_url" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/dialog_buy_eth_options.xml b/app/src/main/res/layout/dialog_buy_eth_options.xml new file mode 100644 index 0000000000..5e084d46f1 --- /dev/null +++ b/app/src/main/res/layout/dialog_buy_eth_options.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_sign_method.xml b/app/src/main/res/layout/dialog_sign_method.xml new file mode 100644 index 0000000000..6cfafcf2a6 --- /dev/null +++ b/app/src/main/res/layout/dialog_sign_method.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_swap_settings.xml b/app/src/main/res/layout/dialog_swap_settings.xml index 54be936f8d..707fc4d1f2 100644 --- a/app/src/main/res/layout/dialog_swap_settings.xml +++ b/app/src/main/res/layout/dialog_swap_settings.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"> + + + + - - diff --git a/app/src/main/res/layout/fragment_webview.xml b/app/src/main/res/layout/fragment_webview.xml index c10189b77a..bd91508732 100644 --- a/app/src/main/res/layout/fragment_webview.xml +++ b/app/src/main/res/layout/fragment_webview.xml @@ -1,15 +1,18 @@ + android:layout_height="match_parent"> - + + android:layout_below="@id/address_bar_widget" /> + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> + android:text="@string/signer_address" /> + android:text="@string/ens_name" + android:visibility="gone" + tools:visibility="visible" /> + android:orientation="horizontal" + android:visibility="gone" + tools:visibility="visible"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_address_icon.xml b/app/src/main/res/layout/item_address_icon.xml index 81f104286c..86db78c3e7 100644 --- a/app/src/main/res/layout/item_address_icon.xml +++ b/app/src/main/res/layout/item_address_icon.xml @@ -111,7 +111,7 @@ app:layout_constraintEnd_toStartOf="@id/guidelineInnerRight" app:layout_constraintStart_toEndOf="@id/guidelineInnerLeft" app:layout_constraintTop_toTopOf="@id/guidelineTop" - tools:src="@drawable/ic_ropsten" /> + tools:src="@drawable/ic_goerli" /> + android:orientation="horizontal" + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> @@ -36,13 +38,20 @@ android:id="@+id/tokens_list" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="3.5" + android:layout_marginHorizontal="@dimen/small_12" + android:layout_weight="@integer/widget_content" android:background="@color/transparent" android:orientation="horizontal" android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_asset_detail.xml b/app/src/main/res/layout/item_asset_detail.xml index cb620c3da8..78348bd35f 100644 --- a/app/src/main/res/layout/item_asset_detail.xml +++ b/app/src/main/res/layout/item_asset_detail.xml @@ -14,17 +14,18 @@ + android:orientation="horizontal" + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> @@ -41,7 +42,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="start" - android:layout_weight="0.6" + android:layout_weight="@integer/widget_control" android:background="@color/transparent" android:padding="@dimen/mini_4" android:scaleType="fitCenter" @@ -81,7 +82,7 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:layout_marginTop="@dimen/small_12" - android:layout_weight="3.5" + android:layout_weight="@integer/widget_content" tools:text="Self Portrait (2021)" /> + + @@ -65,4 +72,5 @@ android:focusable="false" custom:square="true" /> + diff --git a/app/src/main/res/layout/item_balance_display.xml b/app/src/main/res/layout/item_balance_display.xml index 78dbcd65fd..4ebe445c3e 100644 --- a/app/src/main/res/layout/item_balance_display.xml +++ b/app/src/main/res/layout/item_balance_display.xml @@ -11,22 +11,25 @@ + android:orientation="horizontal" + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> @@ -52,8 +55,8 @@ tools:text="35.4236 ETH" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_chain.xml b/app/src/main/res/layout/item_chain.xml new file mode 100644 index 0000000000..7f6d94b741 --- /dev/null +++ b/app/src/main/res/layout/item_chain.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_chain_select.xml b/app/src/main/res/layout/item_chain_select.xml index c9ef1b78ba..19cd634062 100644 --- a/app/src/main/res/layout/item_chain_select.xml +++ b/app/src/main/res/layout/item_chain_select.xml @@ -9,13 +9,13 @@ android:paddingStart="@dimen/small_12" android:paddingEnd="@dimen/tiny_8"> - + tools:src="@drawable/ic_ethereum" /> + tools:text="0xbc9a1026a4bc6f0ba8bbe486d1d09da5732b39e4"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_exchange.xml b/app/src/main/res/layout/item_exchange.xml new file mode 100644 index 0000000000..7cb640cde0 --- /dev/null +++ b/app/src/main/res/layout/item_exchange.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_gas_settings.xml b/app/src/main/res/layout/item_gas_settings.xml index 91b3708a81..54f461162e 100644 --- a/app/src/main/res/layout/item_gas_settings.xml +++ b/app/src/main/res/layout/item_gas_settings.xml @@ -10,16 +10,17 @@ + android:orientation="horizontal" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> @@ -28,7 +29,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/small_12" - android:layout_weight="3" + android:layout_weight="@integer/widget_content" android:orientation="horizontal" android:visibility="gone" tools:visibility="gone"> @@ -54,7 +55,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/small_12" - android:layout_weight="3" + android:layout_weight="@integer/widget_content" android:orientation="horizontal" android:visibility="gone" tools:visibility="gone"> @@ -81,7 +82,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/small_12" - android:layout_weight="3" + android:layout_weight="@integer/widget_content" android:gravity="start" android:text="@string/speed_average" /> @@ -90,27 +91,28 @@ style="@style/Aw.Typography.Control" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="0.6" - android:gravity="end" + android:layout_weight="@integer/widget_control" + android:gravity="start" android:lines="1" android:orientation="horizontal" android:text="@string/edit" android:visibility="gone" - tools:visibility="visible"/> + tools:visibility="visible" /> + android:orientation="horizontal" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> + android:layout_weight="@integer/widget_label" /> + android:layout_height="1dp" + android:layout_weight="@integer/widget_control" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_method.xml b/app/src/main/res/layout/item_method.xml new file mode 100644 index 0000000000..02d9df71d5 --- /dev/null +++ b/app/src/main/res/layout/item_method.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_network_check.xml b/app/src/main/res/layout/item_network_check.xml index 6c75316adb..86791fad8d 100644 --- a/app/src/main/res/layout/item_network_check.xml +++ b/app/src/main/res/layout/item_network_check.xml @@ -14,54 +14,68 @@ android:id="@+id/manage_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="center_vertical" android:background="@color/transparent" - android:src="@drawable/ic_menu" android:contentDescription="@string/manage_tokens" - android:layout_gravity="center_vertical" + android:src="@drawable/ic_menu" android:visibility="gone" - app:tint="?colorControlNormal" - tools:visibility="visible" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="?colorControlNormal" + tools:visibility="visible" /> + android:src="@drawable/ic_ethereum" + app:layout_constraintStart_toEndOf="@id/manage_btn" + app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toEndOf="@id/token_icon" + app:layout_constraintTop_toTopOf="parent"> + + + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/name" + android:layout_marginHorizontal="@dimen/tiny_8" + android:layout_toEndOf="@id/chain_id" + android:text="@string/deprecated" + android:textColor="?colorError" + android:visibility="gone" + tools:visibility="visible" /> - + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_network_display.xml b/app/src/main/res/layout/item_network_display.xml index fb018f2634..12bc36c82c 100644 --- a/app/src/main/res/layout/item_network_display.xml +++ b/app/src/main/res/layout/item_network_display.xml @@ -18,16 +18,17 @@ android:paddingHorizontal="@dimen/standard_16"> @@ -45,6 +46,11 @@ tools:text="Ethereum" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_route.xml b/app/src/main/res/layout/item_route.xml new file mode 100644 index 0000000000..0dc18f33ac --- /dev/null +++ b/app/src/main/res/layout/item_route.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_sign_data.xml b/app/src/main/res/layout/item_sign_data.xml index 5645a8d63a..add3046665 100644 --- a/app/src/main/res/layout/item_sign_data.xml +++ b/app/src/main/res/layout/item_sign_data.xml @@ -12,48 +12,32 @@ - - - - - - @@ -62,11 +46,30 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="start" - android:layout_weight="0.6" + android:layout_weight="@integer/widget_control" android:background="@color/transparent" android:src="@drawable/ic_expand_more" app:tint="?colorControlNormal" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_sign_instance.xml b/app/src/main/res/layout/item_sign_instance.xml index 48bfe415ad..8bcd75653d 100644 --- a/app/src/main/res/layout/item_sign_instance.xml +++ b/app/src/main/res/layout/item_sign_instance.xml @@ -1,43 +1,25 @@ + android:orientation="vertical"> - - - - - - - + custom:headerText="Date" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_simple_widget.xml b/app/src/main/res/layout/item_simple_widget.xml index acaa9f7537..f13c2a8a5b 100644 --- a/app/src/main/res/layout/item_simple_widget.xml +++ b/app/src/main/res/layout/item_simple_widget.xml @@ -15,21 +15,24 @@ android:layout_centerVertical="true" android:gravity="center_vertical" android:orientation="horizontal" - android:padding="@dimen/standard_16"> + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> @@ -49,6 +52,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_standard_header.xml b/app/src/main/res/layout/item_standard_header.xml index ffe880589c..4f6d76c72c 100644 --- a/app/src/main/res/layout/item_standard_header.xml +++ b/app/src/main/res/layout/item_standard_header.xml @@ -1,5 +1,6 @@ + android:visibility="gone" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_ticket.xml b/app/src/main/res/layout/item_ticket.xml index 720006da98..934a8adf7c 100644 --- a/app/src/main/res/layout/item_ticket.xml +++ b/app/src/main/res/layout/item_ticket.xml @@ -13,13 +13,6 @@ android:minHeight="100dp" android:paddingHorizontal="@dimen/tiny_8"> - - + tools:src="@drawable/ic_goerli" /> + tools:src="@drawable/ic_goerli" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_update.xml b/app/src/main/res/layout/item_update.xml index e5684cf6fc..0b543b16f2 100644 --- a/app/src/main/res/layout/item_update.xml +++ b/app/src/main/res/layout/item_update.xml @@ -5,10 +5,13 @@ android:id="@+id/layout_update" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/standard_16" + android:layout_marginHorizontal="@dimen/standard_16" + android:layout_marginBottom="@dimen/standard_16" android:elevation="0dp" android:theme="@style/AppTheme.DarkOverlay" + android:visibility="gone" app:cardBackgroundColor="@color/mine" + app:cardCornerRadius="@dimen/dialog_corner_radius" app:contentPadding="@dimen/standard_16" app:strokeWidth="0dp"> @@ -21,7 +24,7 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_wallet.xml b/app/src/main/res/layout/item_wallet.xml new file mode 100644 index 0000000000..10a0c0d90a --- /dev/null +++ b/app/src/main/res/layout/item_wallet.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_wallet_connect_sessions.xml b/app/src/main/res/layout/item_wallet_connect_sessions.xml new file mode 100644 index 0000000000..d398015cf3 --- /dev/null +++ b/app/src/main/res/layout/item_wallet_connect_sessions.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_wallet_summary_manage.xml b/app/src/main/res/layout/item_wallet_summary_manage.xml index 96823362e3..e15a9c081f 100644 --- a/app/src/main/res/layout/item_wallet_summary_manage.xml +++ b/app/src/main/res/layout/item_wallet_summary_manage.xml @@ -29,6 +29,7 @@ android:layout_height="@dimen/tiny_8" android:layout_marginStart="@dimen/mini_4" android:src="@drawable/ic_wallet_indicator" + tools:ignore="ContentDescription" android:visibility="invisible" /> @@ -62,7 +63,8 @@ + android:layout_height="wrap_content" + android:orientation="horizontal"> @@ -87,11 +90,23 @@ android:lines="1" android:textColor="@color/positive" android:textIsSelectable="true" + android:visibility="gone" app:autoSizeTextType="uniform" tools:text="+123.45%" /> + + + + + + + android:text="@string/vertical_pipe" /> - diff --git a/app/src/main/res/layout/item_warning.xml b/app/src/main/res/layout/item_warning.xml index 52ccf203cf..7513eac151 100644 --- a/app/src/main/res/layout/item_warning.xml +++ b/app/src/main/res/layout/item_warning.xml @@ -5,7 +5,8 @@ android:id="@+id/layout_item_warning" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/standard_16" + android:layout_marginHorizontal="@dimen/standard_16" + android:layout_marginBottom="@dimen/standard_16" android:animateLayoutChanges="true" android:elevation="2dp" android:orientation="vertical" @@ -17,7 +18,7 @@ android:layout_height="wrap_content" android:layout_margin="0dp" app:cardBackgroundColor="@color/mine" - app:cardCornerRadius="8dp" + app:cardCornerRadius="@dimen/dialog_corner_radius" app:cardElevation="0dp"> - + - + + + diff --git a/app/src/main/res/layout/layout_notification_view.xml b/app/src/main/res/layout/layout_notification_view.xml index e96ed1bb91..d99e5865a2 100644 --- a/app/src/main/res/layout/layout_notification_view.xml +++ b/app/src/main/res/layout/layout_notification_view.xml @@ -5,7 +5,8 @@ android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/standard_16" + android:layout_marginHorizontal="@dimen/standard_16" + android:layout_marginBottom="@dimen/standard_16" android:animateLayoutChanges="true" android:clickable="true" android:focusable="true" diff --git a/app/src/main/res/layout/token_selector.xml b/app/src/main/res/layout/token_selector.xml index 6240cc5f16..b26d608fbd 100644 --- a/app/src/main/res/layout/token_selector.xml +++ b/app/src/main/res/layout/token_selector.xml @@ -124,6 +124,7 @@ style="@style/Aw.Typography.Control" android:layout_width="match_parent" android:layout_height="wrap_content" + android:enabled="false" android:gravity="end" android:lines="1" android:text="@string/seekbar_max" @@ -146,4 +147,4 @@ style="@style/Aw.Component.Separator" android:layout_marginTop="@dimen/cozy_20" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/transaction_detail_widget.xml b/app/src/main/res/layout/transaction_detail_widget.xml index 2035a4dc12..f230082b78 100644 --- a/app/src/main/res/layout/transaction_detail_widget.xml +++ b/app/src/main/res/layout/transaction_detail_widget.xml @@ -15,17 +15,17 @@ android:id="@+id/layout_summary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/standard_16" android:gravity="center_vertical" android:orientation="horizontal" - android:paddingTop="@dimen/standard_16" - android:paddingBottom="@dimen/standard_16"> + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> @@ -41,7 +41,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="start" - android:layout_weight="0.6" + android:layout_weight="@integer/widget_control" android:background="@color/transparent" android:padding="@dimen/mini_4" android:src="@drawable/ic_expand_more" @@ -61,24 +61,31 @@ + android:paddingVertical="@dimen/standard_16" + android:paddingStart="@dimen/standard_16" + android:paddingEnd="@dimen/mini_4"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_wc_sessions.xml b/app/src/main/res/menu/menu_wc_sessions.xml new file mode 100644 index 0000000000..f679effe46 --- /dev/null +++ b/app/src/main/res/menu/menu_wc_sessions.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_wc_sessions_delete.xml b/app/src/main/res/menu/menu_wc_sessions_delete.xml new file mode 100644 index 0000000000..13557210db --- /dev/null +++ b/app/src/main/res/menu/menu_wc_sessions_delete.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/raw/init.js b/app/src/main/res/raw/init.js index f83d58f923..c90840cb1e 100644 --- a/app/src/main/res/raw/init.js +++ b/app/src/main/res/raw/init.js @@ -79,8 +79,8 @@ window.AlphaWallet.init(__rpcURL, { } }, { address: __addressHex, - networkVersion: __chainID - //networkVersion: "0x" + parseInt(__chainID).toString(16) || null + //networkVersion: __chainID + networkVersion: "0x" + parseInt(__chainID).toString(16) || null }) window.web3.setProvider = function () { @@ -96,4 +96,4 @@ window.web3.eth.getCoinbase = function(cb) { window.web3.eth.defaultAccount = __addressHex window.ethereum = web3.currentProvider -})(); \ No newline at end of file +})(); diff --git a/app/src/main/res/raw/token_anim.data b/app/src/main/res/raw/token_anim.data new file mode 100644 index 0000000000..20bca12c08 --- /dev/null +++ b/app/src/main/res/raw/token_anim.data @@ -0,0 +1,28 @@ + + + + + +
+ +
+ + diff --git a/app/src/main/res/raw/token_model.data b/app/src/main/res/raw/token_model.data new file mode 100644 index 0000000000..8ab106b939 --- /dev/null +++ b/app/src/main/res/raw/token_model.data @@ -0,0 +1,28 @@ + + + + + +
+ + +
+ + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d0d2eae647..c0b02c4405 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -21,7 +21,6 @@ No, repetir Transferir %s Comprar %s - Inténtalo de nuevo ¿Ya tienes un monedero? Crear un monedero nuevo @@ -68,8 +67,6 @@ Más detalles Añadir monedero Ver en Block Explorer - Coinbase - LocalEthereum Changelly (compra con tarjeta de crédito) Detalle de la transacción @@ -84,8 +81,6 @@ N/A El monedero no está seleccionado Las actividades aparecerán aquí - -- - -- Enviando... Obtener dirección a partir del código QR Cuanto más alto sea el precio del gas, más cara será la tarifa de tu transacción, pero más rápido se procesará por la red Ethereum. @@ -97,7 +92,6 @@ Error al exportar el monedero Error al cargar la lista de tokens Error al crear un monedero - AlphaWallet Todo Monedas Opciones avanzadas @@ -115,9 +109,7 @@ Canjear Tickets canjeados Usar token - %1$s%2$s - %1$s (%2$s) Crear orden de venta Selecciona %1$s para vender: Selecciona los tickets para canjear: @@ -181,11 +173,9 @@ DD/MM/YY - ETH Cantidad de tickets Coste total: Equivalente en USD - %1s %2s Selecciona la cantidad de %1$s Tu ticket ha sido canjeado Buscar: @@ -198,7 +188,6 @@ ¿Qué es una frase semilla? ¿Cómo transfiero ETH a mi monedero? ¿Qué es TokenScript? - tokenscript_explaination.html 24 horas Valoración %1s/ticket @@ -241,7 +230,6 @@ Compartir MagicLink Generar MagicLink Anunciar en el mercado - --:-- Hora de expiración del MagicLink Selecciona una hora de expiración Selecciona una fecha de expiración @@ -265,11 +253,6 @@ Importante 0,00 ETH Cambiar monedero - Facebook - Twitter - ERC 20 - ERC 875 - ERC 875* Ethereum es una plataforma informática y un sistema operativo de código abierto, público, de distribución basada en las cadenas de bloques, con funcionalidad de contratos inteligentes (scripting). Es la infraestructura de los productos y servicios de cadena de bloques.\n\nEl ETH/Ether es una criptomoneda cuya cadena de bloques la genera la plataforma Ethereum. El Ether se puede transferir entre cuentas y se puede utilizar para compensar a los nodos mineros participantes por los cálculos realizados. Ethereum proporciona una máquina virtual de Turing completo descentralizada, la Ethereum Virtual Machine (EVM), que puede ejecutar scripts usando una red internacional de nodos públicos. El \"gas\", un mecanismo interno de fijación de precios para las transacciones, se emplea para mitigar el spam y asignar los recursos en la red. En la actualidad, Ethereum es la mayor y mejor cadena de bloques para el desarrollo de aplicaciones y servicios de contratos inteligentes. AlphaWallet comienza apoyando la cadena de bloques de Ethereum y ayuda a mejorar su tecnología. Puedes convertir el Ethereum en dinero fíat y transferirlo a tu cuenta bancaria a través de cualquier plataforma de intercambio de criptomonedas. @@ -384,14 +367,12 @@ Reclamar criptomoneda gratuita Importación de monedas recibirás - Escanear código QR Resultado del escaneo no válido Dirección de Ethereum Acabas de escanear una dirección de Ethereum. ¿Quieres probar a cargarla como un token? Cargar token Cadena de bloques de %1$s - 0,00 %s Telegrama (Atención al Cliente) Solicitud de transferencia Solicitud de transferencia de tokens: %1$s @@ -426,6 +407,7 @@ Busca o escribe una dirección web Redes Seleccionar redes activas + Redes habilitadas (%s) Coleccionables Función del token Canal de notificación de AlphaWallet @@ -534,7 +516,6 @@ entrada de la función \"%1$s\" no encontrada en los elementos HTML. Valor de entrada no válido: %1$s Valor - LinkedIn No ha sido posible crear la clave. Prueba a activar la seguridad en tu teléfono. Una frase semilla, frase semilla de recuperación o frase semilla de respaldo es una lista de palabras que almacena toda la información necesaria para recuperar un monedero criptográfico. Normalmente, el software del monedero genera una frase semilla e indica al usuario que la escriba en un papel. Transacción demasiado grande @@ -543,10 +524,8 @@ Detalles de la firma Copiar la dirección del contrato Auto - ERC721T Error al firmar el mensaje de canje Tipo de token - Reddit Compatibilidad de TokenScript Versión Depuración de TokenScript @@ -584,10 +563,8 @@ Consola Borrar la caché del navegador Recargar datos del Token - TokenScript Cambiar idioma Añadir / Ocultar tokens - Instagram Cambiar moneda Seleccionar moneda FÍAT Error de archivo de TokenScript @@ -619,7 +596,6 @@ Parámetro no válido durante la llamada a la transacción de TokenScript. Revisa las entradas. TokenScript sustituido por %1$s%2$s\n\n Tokens de origen:\n\n - %1$s -> %2$s\n\n (Depuración) Confirmar la eliminación del archivo Eliminar el archivo %1$s @@ -655,7 +631,6 @@ Tokens ocultos Ignorar Buscar tokens... - WalletConnect Finalizar sesión Enviar transacción ETH Rechazada por el usuario @@ -664,7 +639,6 @@ Transacción rechazada (prueba a utilizar más gas) de: %1$s de - %1$s %2$s a: %1$s aprobado para transferir tokens: %1$s para gastar en nombre de: %1$s @@ -680,10 +654,8 @@ La sesión anterior de WalletConnect no finalizó correctamente. Regrese a la página web y desconéctese, luego inicie una nueva sesión. Datos QR de WalletConnect no válidos Connection attempt timed out. Please cancel the request from the Dapp and start a new session. - Nonce Datos Transacciones Firmadas - %1$s ¿Eliminar este registro de sesión? Session has been terminated. Switch back to Dapp and start a new WalletConnect session. Transaction not sent @@ -769,8 +741,6 @@ In order to speed up this transaction, the gas price must be at least 10% higher than the original gas price. Speed Up Setting Cancel Setting - Email - Discord Select %s Selected This site is requesting you to switch to the %1$s chain with chain ID: %2$s. This will reload the page. @@ -835,7 +805,6 @@ Performance Search token Assets - DeFi Gobernancia Coleccionables ¿Qué hay de nuevo? @@ -849,10 +818,9 @@ Unkown Network This dapp is requesting to change to an unknown chain: %1$s - No Active sessions + Sin sesiones activas Connect Wallet La frase de semillas solo puede contener palabras - Token # External Link Description Details @@ -865,7 +833,6 @@ I already have a Wallet Select Mode Network Info - Testnet Website Light Dark @@ -876,6 +843,12 @@ Use 1559 Transactions Experimental 1559 Transactions Reiniciar + Métodos + Conexiones activas a Dapp basado en navegador + El usuario desconecta la sesión. + Nombre + Billetera + La billetera no existe o es solo reloj Chain ID: %1d Created By Token Standard @@ -889,7 +862,6 @@ Unable to store key: %1$s ENS Lookup Warning AlphaWallet has detected a discrepancy between ethereum\'s timestamp and your device\'s current time, or possibly the blockchain connection is not synchronized, please check your phone\'s time is correct, or ignore this warning. - 0 Disminuir Incrementar The transaction has timed out. You can view the status in activity page. @@ -914,19 +886,71 @@ under 1 second over 1 second not responding + No se admite la cadena %s. + La red %s debe estar habilitada antes de conectarse. + Select Token + Custom + WalletConnect está activa + Haga clic para ver las sesiones activas Modo de red de prueba ¿Dónde están mis tokens? No te preocupes. Tus tokens están a salvo. Estás viendo redes Testnet. Los desarrolladores los utilizan para probar nuevos diseños. Puede cambiar a Mainnet en cualquier momento. Cambiar a red principal - Select Token - Custom Name Note - URL Connect Connect to Sign This will inform the remote site your wallet address is %s Floor Price Average Price + Provider + Delete Empty + Delete All + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + Isuficiente %1$s saldo + Transferencia segura + Política de privacidad + Términos de servicio + Billeteras conectadas + Rareza + Intercambios preferidos + Tarifa de gasolina: %s + Fetching Routes + Obtención de rutas + Seleccionar intercambios + Nuevas rutas en %s + Cantidad a intercambiar + Sitio web del proveedor + Detalles de cotización + Intercambiar a través de %s + No se encontraron rutas para los parámetros dados. + Seleccione al menos un intercambio. + Obsoleta + No se pudo realizar la acción en esta billetera de solo reloj. + Escaneo en curso: espere a que finalice + ¿Habilitar cadenas para tokens seleccionados? + (Sin título) + Detalles de la sesión + Propuesta de sesión + Monedero diferente del monedero activo. + Analítica + Informes de bloqueo + Ayúdenos a mejorar AlphaWallet compartiendo sus datos anónimos con nosotros. Esto no incluye ninguna información financiera. + ¿Compartir datos anónimos? + ¿Habilitar las cadenas necesarias? + Los tokens seleccionados están en cadenas actualmente no seleccionadas, ¿habilitar cadenas no seleccionadas? diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index aed3fecca2..6033f3a41c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -22,7 +22,6 @@ Non, recommencer Transférer %s Acheter %s - Essayer encore Avez vous déja un portefeuille? Créer un nouveau portefeuille @@ -75,8 +74,6 @@ Plus de détails Ajouter portefeuille Voir sur Block Explorer - Coinbase - LocalEthereum Changelly (Acheter avec une carte de crédit) Détail de la Transaction Impossible de générer le code QR. @@ -90,8 +87,6 @@ N/A le portefeuille n\'est pas sélectionné Les activités apparaîtront ici - -- - -- Envoi en cours... Obtenir l\'adresse à partir du code QR Plus le prix du gaz est élevé, plus vos frais de transaction seront élevés, mais plus votre transaction sera traitée rapidement par le réseau Ethereum. @@ -104,7 +99,6 @@ Echec du chargement de la liste des tokens Impossible de créer un portefeuille - AlphaWallet Tous Devises Options avancées @@ -122,9 +116,7 @@ Racheter Tickets Rachetés Utiliser Token - %1$s%2$s - %1$s (%2$s) Créer un ordre de vente Selectionner %1$s à vendre: Selectionner les tickets à racheter:montrer @@ -189,11 +181,9 @@ DD/MM/YY - ETH Quantité de Tickets Coût total: Equivalent en USD - %1$s %2$s Selectionner Quantité of %1$s Votre ticket a été racheté Chercher: @@ -206,11 +196,6 @@ Qu\'est ce qu\'une Phrase Seed? Comment transférer de l\'ETH dans mon portefeuille? Qu\'est ce que TokenScript? - Politique de Confidentialité - Termes de Service - privacyPolicy.html - termsOfService.html - tokenscript_explaination.html 24 heures Appréciation %1$s/Ticket @@ -253,7 +238,6 @@ Partager le lien Magique Générer le lien Magique Lister sur la marketplace - --:-- Date d\'expiration du lien Magique Veuillez selectionner l\'heure d\'expiration Veuillez selectionner la date d\'expiration @@ -277,11 +261,6 @@ Important 0.00 ETH Changer de portefeuille - Facebook - Twitter - ERC 20 - ERC 875 - ERC 875* Ethereum est une plate-forme informatique distribuée et un système d\'exploitation open source, public et basé sur la blockchain, avec une fonctionnalité de contrat intelligent (script). C\'est l\'infrastructure des produits et services blockchain.\n\nETH/Ether est une crypto-monnaie dont la blockchain est générée par la plateforme Ethereum. L\'éther peut être transféré entre comptes et utilisé pour compenser les nœuds de minage des participants pour les calculs effectués. Ethereum fournit une machine virtuelle décentralisée complète de Turing, la machine virtuelle Ethereum (EVM), peut exécuter des scripts à l\'aide d\'un réseau international de nœuds publics. «Gas», un mécanisme interne de tarification des transactions, est utilisé pour atténuer le spam et allouer des ressources sur le réseau. Actuellement, Ethereum est la meilleure et la plus grande blockchain pour le développement d\'applications et de services de contrats intelligents. AlphaWallet supporte la blockchain Ethereum et aide à améliorer sa technologie. Vous pouvez convertir votre Ethereum en monnaie fiat et les tranférer sur votre compte bancaire avec n\'importe quel échange crypto. @@ -397,14 +376,12 @@ Accepter baisse de devises Importation Devises vous recevrez - Scannez le code QR Résultat du scan Invalide Adresse Ethereum Vous venez de scanner une adresse Ethereum. Voulez-vous essayer de la charger en tant que token? Chargement de token %1$s Blockchain - 0.00 %s Telegram (Support Client) Demande de transfert Demande de transfert de token: %1$s @@ -439,6 +416,7 @@ Chercher ou taper une adresse web Réseaux Selectionner Réseaux Actifs + Réseaux Activés (%s) Objects de Collections Fonctions du Token Chaine de notifications Alphawallet @@ -547,7 +525,6 @@ entrée de fonction \'%1$s\' introuvable dans les éléments HTML. Valeur d\'Entrée Invalide: %1$s Valeur - LinkedIn Impossible de créer la clé. Essayer d\'activer la sécurité de votre téléphone. Une phrase seed, une phrase de récupération seed ou une phrase seed de sauvegarde est une liste de mots qui stockent toutes les informations nécessaires pour récupérer un portefeuille cryptographique. Le logiciel de portefeuille génère généralement une phrase seed et demande à l\'utilisateur de l\'écrire sur papier. Transaction Trop Grande @@ -556,10 +533,8 @@ Détails Signature Copier l\'adresse du Contrat Soi - ERC721T Échec de la signature du message d\'échange Type de Token - Reddit Comptabilité TokenScript Version TokenScript debug @@ -598,10 +573,8 @@ Console Effacer le Cache du Navigateur Recharger les données du Tokens - TokenScript Changer Langue Ajouter / Cacher Tokens - Instagram Changer Devise Selectionner Devise FIAT TokenScript fichier erreur @@ -634,7 +607,6 @@ Paramètre non valide lors de l\'appel de TokenScript Transaction. Veuillez vérifier les entrées. TokenScript remplacé par %1$s%2$s\n\n Tokens d\'Origine:\n\n - %1$s -> %2$s\n\n (Debug) Confirmer la Suppression du Fichier Supprimer fichier %1$s @@ -670,7 +642,6 @@ Tokens Cachés Ignorer Chercher des Tokens... - WalletConnect Terminer Session Envoyer une Transaction ETH Rejeté par l\'utilisateur @@ -679,7 +650,6 @@ Transaction rejetée (essayer d\'utiliser plus de gas) de: %1$s de - %1$s %2$s à: %1$s Approuvé pour transférer des tokens: %1$s à dépenser au nom de: %1$s @@ -689,17 +659,14 @@ Approuvé Approbation accordé appel de TokenScript: - Testnet Aujourd\'hui Aller au Token Session WalletConnect Invalide La session précédente de WalletConnect ne s\'est pas correctement terminée. Veuillez retourner à la page Web et vous déconnecter, puis démarrer une nouvelle session. Données QR WalletConnect Invalide Connection attempt timed out. Please cancel the request from the Dapp and start a new session. - Nonce Puissance Transactions Signés - %1$s Supprimer cet enregistrement de session? La session a été terminée. Revenez à Dapp et démarrez une nouvelle session WalletConnect. Transaction pas envoyée @@ -796,8 +763,6 @@ %1$s Assets | %2$s This is Testnet Swap with Quickswap - Email - Discord Name This Wallet Enter Wallet Name Save Name @@ -853,7 +818,6 @@ Performance Search token Assets - DeFi Gouvernance Collectibles Quoi de neuf? @@ -867,10 +831,9 @@ Unkown Network This dapp is requesting to change to an unknown chain: %1$s - No Active sessions + Aucune session active Connect Wallet La phrase de graine ne peut contenir que des mots - Token # External Link Description Details @@ -893,6 +856,12 @@ Use 1559 Transactions Experimental 1559 Transactions Réinitialiser + Méthodes + Connexions actives à Dapp basé sur un navigateur + L\'utilisateur déconnecte la session. + Nom + Portefeuille + Le portefeuille n\'existe pas ou il ne s\'agit que d\'une montre Chain ID: %1d Created By Token Standard @@ -906,7 +875,6 @@ Unable to store key: %1$s ENS Lookup Warning AlphaWallet has detected a discrepancy between ethereum\'s timestamp and your device\'s current time, or possibly the blockchain connection is not synchronized, please check your phone\'s time is correct, or ignore this warning. - 0 Diminuer Augmenter The transaction has timed out. You can view the status in activity page. @@ -931,19 +899,71 @@ under 1 second over 1 second not responding + La chaîne %s n\'est pas prise en charge. + Le réseau %s doit être activé avant la connexion. + Select Token + Custom + WalletConnect est actif + Cliquez pour voir les sessions actives Mode Testnet Où sont mes jetons ? Ne t\'en fais pas. Vos jetons sont en sécurité. Vous visualisez les réseaux Testnet. Ils sont utilisés par les développeurs pour tester de nouveaux designs. Vous pouvez passer à Mainnet à tout moment. Passer au réseau principal - Select Token - Custom Name Note - URL Connect Connect to Sign This will inform the remote site your wallet address is %s Floor Price Average Price + Provider + Delete Empty + Delete All + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + Solde de %1$s insuffisant + Transfert de sécurité + Politique de confidentialité + Conditions d\'utilisation + Portefeuilles connectés + Rareté + Échanges préférés + Frais de gaz: %s + Récupération d\'itinéraires + Sélectionnez l\'itinéraire + Sélectionnez les échanges + Nouvelles routes dans %s + Montant à échanger + Site Web du fournisseur + Détails du devis + Échange via %s + Aucune route trouvée pour les paramètres donnés. + Sélectionnez au moins un échange. + Obsolète + Impossible d\'effectuer une action sur ce portefeuille Watch-only. + Numérisation en cours : veuillez patienter jusqu\'à la fin + Activer les chaînes pour les jetons sélectionnés ? + (Pas de titre) + Détails de la session + Proposition de séance + Portefeuille différent du portefeuille actif. + Analytique + Rapport d\'incident + Aidez-nous à améliorer AlphaWallet en partageant vos données anonymes avec nous. Cela n\'inclut aucune information financière. + Partager des données anonymes ? + Activer les chaînes requises? + Les jetons sélectionnés sont sur des chaînes actuellement non sélectionnées, activer les chaînes non sélectionnées? diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..d60e23706e --- /dev/null +++ b/app/src/main/res/values-id/strings.xml @@ -0,0 +1,959 @@ + + + + Kirim + Send %1$s + Penerima + Bagian ini wajib diisi + Pengaturan + Dompet + Pastikan bahwa Anda memiliki cadangan dompet ini. + salin ke papan klip + kirim + Diterima + OK + Format barcode tidak berlaku: %s + Apakah Anda yakin ingin menghapus dompet? + + Kesalahan mengimpor kata kunci + Kesalahan menghapus dompet + Apakah Anda berhasil membuat cadangan? + Ya, lanjutkan + Tidak, ulangi + Transfer %s + Membeli %s + Coba lagi + Sudah memiliki dompet? + Membuat dompet Baru + Telah terjadi kesalahan. + Tidak ditemukan token. + + + Alamat Saya + kirim + Konfirmasi + Rincian Transaksi + + + Kesalahan + Penanganan… + Tambahkan dompet + Bagaimana Anda ingin membeli? + + + dompet diimpor + Cadangan telah dibuat + Kode QR tidak berisi alamat yang berlaku + Pemindaian QR memerlukan Android 7.0 (API level 24) atau di atasnya. + + + Dari + Kepada + Batas Gas + Harga Gas + Harga (GWEI) Gas + Biaya Maksimum (GWEI) + Harga Gas Lama + Biaya Jaringan (Maksimum) + OK + + + Dari + Kepada + Biaya Gas + Harga Gas + Gas yang Digunakan + Biaya Jaringan + Transaksi # + Transaksi Hash # + Waktu Transaksi + Blok # + Blok + + + Bagikan + Lebih detail + Tambahkan dompet + Lihat di Blok Penjelajah + Changelly (Beli dengan kartu kredit) + + Detail transaksi + Tidak dapat membuat kode QR. + Alamat tidak valid + Jumlah yang tidak valid + Kesalahan transaksi + perangkat mengindikasikan transaksi mungkin gagal. Lanjutkan? + Gagal mengimpor token + harus berupa nilai angka. + Transaksi berhasil + N/A + Dompet tidak dipilih + Aktivitas akan muncul di sini + Mengirim… + Dapatkan alamat dari Kode QR + Semakin tinggi harga Gas, semakin mahal biaya transaksi Anda, tetapi semakin cepat transaksi Anda akan diproses oleh jaringan Ethereum. + Batas Gas mencegah smart contract menghabiskan semua Ethereum Anda. We will try to calculate the gas limit automatically for you, but some smart contracts may require a custom gas limit. + Lanjutan + + Tidak dapat menambahkan token. + Sudah ditambahkan + Gagal mengekspor dompet + Gagal memuat daftar token + Gagal membuat dompet + + Semua + Mata Uang + Pilihan Lanjutan + Salin Alamat Dompet + Ubah nama dompet ini + Nama + Lokasi + Tanggal + Harga + + Menukarkan + Menjual + Transfer + alamat + menukarkan + Tiket Ditukarkan + Gunakan Token + + Membuat Pesanan Penjualan + Pilih %1$s untuk menjual: + Pilih Tiket untuk Ditukarkan:show + Pembelian + Dompet + Pengaturan + Bantuan \u0026 TANYA JAWAB + Transaksi + Alamat Dompet Saya + Pemberitahuan + Menukarkan Tiket + Menunggu blockchain … + Mendaftar + Saring berdasarkan: + Tanggal + Harga + Jarak + Jangka waktu + Menetapkan Harga: + Pelajari lebih lanjut tentang Ethereum + Harga Per Tiket + Menetapkan harga dalam %1$s (%2$s) + Transfer Tiket + MagicLink akan berlaku sampai %1$s %2$s + Menghasilkan MagicLink + Transfer Sekarang + kepada + + + Paling Terbaru + Tertua + + + Rendah ke Tinggi + Tinggi ke Rendah + + + Apa saja + 1km + 5km + 10km + + + Apa saja + 1 minggu + 1 bulan + 1 tahun + + + Apa saja + 1–64 + + + Apa saja + VIP + + + Apa saja + 2 + 3 + + + HARI/BULAN/TAHUN + + Jumlah Tiket + Total Biaya: + Setara dalam USD + Pilih Kuantitas of %1$s + Tiket Anda telah ditukarkan + Cari: + Kelas + Nomor. Kursi Bersama + Cocok + Apa itu ETH/Ethereum? + Mengapa aWallet menggunakan Ethereum? + Bagaimana cara mendapatkan uang saya? + Apa yang dimaksud dengan Seed Phrase? + Bagaimana cara mentransfer ETH ke dompet saya? + Apa itu TokenScript? + 24 jam + Penghargaan + %1$s/Tiket + Ethereum + Blockchain Ethereum + Penerbit: + KEAMANAN PERANGKAT TERGANGGU + Ponsel Anda di-root tetapi kuncinya masih dilindungi oleh enkripsi enklave Android. Vektor serangan akan berasal dari aplikasi papan ketik atau tampilan papan klip yang mempelajari kunci Anda ketika Anda memasukkannya. AlphaWallet melakukan yang terbaik untuk mengurangi risiko ini dengan mematikan saran otomatis papan ketik. Jika Anda menjalankan root, merekomendasikan Anda hanya menggunakan aplikasi papan ketik arus utama, rjalankan aplikasi anti-virus berkualitas baik dan gunakan pengelola root sumber terbuka yang terpercaya seperti Magisk atau SuperSU untuk membatasi akses ke hak akses root. Pada akhirnya, keselamatan kunci adalah tanggung jawab Anda. + Masih membutuhkan bantuan? Hubungi kami + Konfirmasi Penjualan? + Batalkan dan Kembali + Maksimum + Minimum + $0.00 + Konfirmasi Pembelian + Rincian Tiket Transfer + Tiket Transfer + Pilih Tiket untuk Transfer: + Buka Daftar Saya + Tiket Anda telah disiapkan untuk dijual di Tempat pemasaran + Perdagangan tidak diproses + OK + Dompet + Transaksi + Browser + Pengaturan + Aktivitas + Mengimpor Token + tiket + Tiket + Gagal + Total biaya untuk %1$d %2$s: + Impor Tiket + Batalkan + Konfirmasi Pembelian + Tautan Impor Tidak Valid + Tautan impor mengandung kesalahan; harap minta ulang tautan tersebut + DEPRESIASI + Buat tautan transfer gratis? + Bagikan MagicLink + Hasilkan MagicLink + Daftar di tempat pemasaran + MagicLink Waktu Kadaluwarsa + Silakan pilih waktu kadaluwarsa + Silakan pilih tanggal kadaluwarsa + Pemasukan harus lebih besar dari 0 + MagicLink Tanggal Kadaluarsa + Total + Konfirmasi Rincian Transfer + Pengaturan Lanjutan + Konfirmasi + Pindai kode QR atau salin teks di bawah ini: + Alamat Dompet Saya + Disalin ke papan klip + MagicLink kedaluwarsa pada: + jumlah: %s + Set MagicLink Expiry: + %1$s %2$s Terpilih + %1$s %2$s/Tiket + MagicLink akan dibuat untuk memungkinkan pembeli membeli tiket Anda. + HARI/BULAN/TAHUN + Sebelum tautan berakhir, siapa pun yang memiliki MagicLink dapat membeli tiket Anda dengan satu klik. + Penting + 0.00 ETH + Ubah Dompet + Ethereum adalah sistem yang terbuka, secara umum, platform komputasi terdistribusi berbasis blockchain dan sistem operasi yang menampilkan fungsionalitas Smart Contract (scripting). Ini adalah infrastruktur untuk produk dan layanan blockchain.\n\nETH/Ether adalah mata uang kripto yang blockchain-nya dihasilkan oleh platform Ethereum. Ether dapat ditransfer antar akun dan digunakan untuk mengkompensasi node penambangan peserta untuk perhitungan yang dilakukan. Ethereum menyediakan mesin virtual lengkap yang terdesentralisasi, Mesin Virtual Ethereum (EVM), yang dapat mengeksekusi skrip menggunakan jaringan internasional node publik. "Gas", mekanisme penetapan harga transaksi internal, digunakan untuk mengurangi spam dan mengalokasikan sumber daya pada jaringan. + Saat ini, Ethereum adalah blockchain terbaik dan terbesar untuk mengembangkan aplikasi dan layanan Smart Contract. AlphaWallet dimulai dengan mendukung blockchain Ethereum dan membantu meningkatkan teknologinya. + Anda dapat mengubah Ethereum menjadi uang fiat dan mentransfernya ke rekening bank Anda melalui bursa mata uang kripto apa pun. + Anda bisa memberikan alamat dompet Anda kepada pengirim. Mereka dapat mentransfer ETH kepada Anda melalui alamat dompet Anda. + Tiket Berlaku untuk Impor + Tiket sudah diimpor + Tiket diimpor sebagian + Impor Gratis + Pilih %1$s Kuantitas: + Pilih Metode Transfer: + Tetapkan MagicLink Masa Berlaku: + Masukan Alamat Transfer : + Impor Dompet + Impor + Kata sandi + Masukkan Kata Sandi + Masukkan kata kunci JSON + Seed + Kata Kunci + Kata kunci pribadi + Simbol + Bilangan desimal + Tambahkan Token Khusus + Kata Kunci pribadi harus sepanjang 64 karakter + 15 + 160 + Pendeteksi QR + Pilih Bahasa: + Ubah Bahasa + Tebus + Menerima dari + Transfer + Transfer Dari + Memuat Tiket Baru + Terima dari Magiclink Gratis + Transfer Magiclink + Mengakhiri Kontrak + Kontraktor + Operasi Tidak Berlaku + Administrasi Penukaran + Terima dari Magiclink + Transfer Magiclink + Dijual melalui Magiclink + Pembelian Magiclink + Mint + Bakar + Selanjutnya + Simpan + Info Kontrak + Alamat Kontrak + Alamat Ethereum atau nama ENS + Nama ENS + Nama ENS Anda + Magiclink Kedaluwarsa + Token Tidak Berlaku + Tidak ada kontrak yang berlaku di alamat ini pada jaringan ini. Kontrak bisa saja sudah diputuskan atau mungkin berada di jaringan yang berbeda. Coba ubah ke \'Ropsten (Uji)\'. + Pembaruan Tersedia + Pembaruan versi tersedia di situs web AlphaWallet. + Unduh dan Pasang + Nanti + Pembaruan Alphawallet + Tidak dapat memperbarui Aplikasi + Izin tulis penyimpanan tidak diberikan. + Lewati hingga rilis berikutnya + Aktifkan Pengesampingan XML + Aktifkan XML akan membuat direktori \'AlphaWallet\' di penyimpanan ponsel Anda. File konfigurasi Token (XML) dapat dimasukkan ke dalam direktori ini yang akan mengesampingkan konfigurasi Token default atau jaringan. + Apakah Anda ingin mengaktifkan Token konfigurasi direktori? + Perjanjian + Perambah DApp + Pesan + Alamat + Pemohon + Tandai Pesan + Tandai Pesan Pribadi + Tandai pesan yang diketik + Tolak + Disetujui + Nilai Transaksi + Tanda Transaksi + Lihat Penanda + ERC721 + Tertunda + Data Tidak Lengkap + Rincian Token + Atribut + Atribut-atribut + Buka pada %1$s + Memuat ulang + Impor Gratis (dengan biaya gas) + Konfirmasi transaksi DApp" + Alamat yang Diselesaikan + Konfirmasi Transfer Token + Memeriksa nama blockchain + Alokasikan ke + Menyetujui + Memeriksa layanan bebas Gas + ERC20 Detail + kirim + Tersedia + Dollar America + Transaksi Terkini + Anda belum memiliki transaksi + Tempel + Ketika Anda mengirim atau menerima %1$s,\n transaksi Anda akan muncul di sini + ether + token + Transaksi Tidak Berlaku + Transaksi DApp tidak berisi data + Tidak mencukupi %1$s + Sertifikat SSL Tidak Berlaku. Situs ini mungkin tidak aman. Lanjutkan? + Impor Token + Ambil Penurunan Mata Uang + Impor Mata Uang + Anda akan menerima + Pindai Kode QR + Hasil Pemindaian Tidak valid + Alamat Ethereum + Baru saja memindai alamat Ethereum. Apakah Anda ingin mencoba mengunggah ini sebagai Token? + Ungu Token + %1$s Blockchain + Telegram (Layanan Pelanggan) + Permintaan transfer + Permintaan transfer token: %1$s + Membuat Permintaan Pembayaran + Masukkan jumlah yang diminta: + Layanan Feemaster saat ini tidak tersedia, silakan coba lagi nanti. + Mengimpor Token + Nama Dompet + Dompet %1$d + Token Tidak Ditemukan + Tidak ada kontrak Token yang ditemukan di alamat ini pada Ethereum Chain yang aktif. + Selamat datang di web yang terdesentralisasi + Hapus + Hapus DApp + Hapus %1$s dari DApps Saya? + Hapus Riwayat + Hapus + Apakah Anda yakin telah menghapus riwayat perambah (browser) Anda? + Riwayat perambah (browser) Anda muncul di sini. + Riwayat Perambah (browser) + Anda belum memiliki DApps yang ditampilkan.\n Mulai dengan menjelajahi perambah (browser). + DApps saya + Tambahkan + Edit + Riwayat + Temukan DApps + Tambahkan + Tambahkan ke DApps Saya + Edit DApp + Judul + Alamat + Cari atau ketik alamat situs jaringan + Jaringan + Pilih Jaringan Aktif + Jaringan yang Diaktifkan (%s) + Barang Koleksi + Fungsi Token + Saluran pemberitahuan Alphawallet + Izin untuk menggunakan kamera ditolak. + Konfirmasi Transaksi TokenScript + Kirim %1$s %2$s to %3$s (%4$s) + Dana saat ini: %1$s %2$s + %1$s bukan alamat ethereum yang berlaku + Jaringan Aktif + Melakukan pemindaian untuk Token + Pemilihan Token + Fungsi ini memerlukan token %1$s + %1$s Token + Transaksi tidak memiliki penerima + Transaksi DApp tidak mengandung nilai. + Dompet Anda tidak memiliki cadangan! + Anda belum membuat cadangan dompet Anda. Anda memiliki $200 USD. Lakukan sekarang. + BACK UP DOMPET %1$s → + AOtentikasi Dibatalkan + Otentikasi Gagal + Cadangkan dompet Anda dengan seed phrase + Membuat cadangan sangat sederhana dan aman: cukup tuliskan 12 kata dan simpan di tempat rahasia, tanpa koneksi internet. + Cadangkan dompet saya + Tuliskan seed phrase\di atas kertas + OK, Saya akan mencatat ini + Verifikasi Seed Phrase + Lanjutkan + phrase pemulihan tidak valid. Mohon periksa dan coba lagi. + Mengelola Dompet + Tunjukkan Seed Phrase + Dompet ini hilang + Jika Anda kehilangan akses ke perangkat ini, dana Anda akan hilang, kecuali jika Anda mencadangkannya! + Hati-hati saat menghapus dompet Anda. Jika Anda belum mencadangkan dompet Anda, Anda mungkin kehilangan semua dana Anda. + Ekspor JSON Penyimpanan Kunci + Apa itu Penyimpanan Kunci JSON? + Penyimpanan Kunci adalah berkas teks. Anda bisa menyalin kontennya dan menempatkannya ketika Anda ingin mengimpor dompet Anda. + Uji seed phrase saya + Membuat cadangan sangat sederhana dan aman, cukup tuliskan kata %1$s dan simpan di tempat yang aman, tanpa koneksi internet. + Penyimpanan Kunci adalah berkas teks. Anda bisa menyalin kontennya dan menempatkannya ketika Anda ingin mengimpor dompet Anda. Ini adalah cara yang aman untuk mencadangkan dompet. + Atur Kata Sandi Penyimpanan Kunci + CATATAN: Anda harus mengingat kata sandi Anda. Kami tidak menyimpan kata sandi Anda di tempat lain. pemyimpanan kunci selalu dienkripsi, Jika tidak, siapa pun yang memilikinya bisa mendapatkan uangnya. + Simpan Penyimpanan Kunci + Jangan bagikan cadangan Anda. Anggota tim AlphaWallet tidak akan memintanya. + Masukkan seed phrase + Jika seed phrase Anda tidak dalam bahasa Inggris, feel free to convert it to English yourself using tools like these https://iancoleman.io/bip39/ + Phrase pemulihan yang tidak valid. Mohon periksa dan coba lagi. + IMPOR DARI ICLOUD/DROPBOX/GOOGLE DRIVE + Masukkan kunci pribadi + CATATAN: Anda harus mengingat kata sandi Anda. Kami tidak menyimpan kata sandi Anda di tempat lain. KeyStore is always encrypted, Jika tidak, siapa pun yang memilikinya bisa mendapatkan uangnya. + Pkunci pribadi hanya berupa hex: gunakan a-f A-F dan 0-9 + Tidak ada yang perlu dibagikan + Jelajahi DApp untuk membagikannya. + Saatnya membuat cadangan Dompet Anda + Kami sangat menyarankan untuk membuat cadangan dompet Anda setiap bulan. + Gunakan Pin + Sentuh sensor sidik jari + Buka Kunci Pribadi + Perangkat Tidak Aman + Perangkat Anda tidak terkunci oleh Sidik Jari, PIN atau Biometrik lainnya. Tidak dapat membuat kunci pribadi yang aman sampai perangkat memiliki perlindungan. + Ingatkan saya nanti + Ubah Dompet + Dompet Warisan + Pencadangan ditunda. + Kesalahan Kunci + Kata sandi 6 karakter atau lebih + Membuat Dompet baru + Perhatikan + Saya sudah memiliki dompet: + Perhatikan-Dompet saja + Masukkan Alamat Ethereum + Alamat Ethereum: 0x kemudian 40 karakter 0-9 a-f A-F + Perhatikan Dompet + Menerima Pembayaran + Cadangkan Dompet Anda + Dana Anda akan berisiko jika Anda tidak membuat cadangan sebelum menggunakan dompet Anda. + Tutup + Cadangkan Dompet ini + Tidak Terkunci + Cadangkan sekarang! + Jangan bagikan kunci Anda + Kunci Seed Phrase Anda untuk meningkatkan keamanan + Keamanan AlphaWallet sedang ditingkatkan menjadi yang terbaik yang dapat dicapai. Kunci frasa benih Anda dengan biometrik. + Tingkatkan keamanan kunci + Kami sangat menyarankan Anda membuat cadangan dompet Anda. + Lewati Pembaruan + Penyimpanan Kunci Anda untuk meningkatkan keamanan + Penyimpanan kunci JSON Tidak Valid + Kunci pribadi tidak berlaku + Dompet %1$s sudah tersedia, apakah Anda ingin mengimpor kembali dompet? + Mengimpor kembali Dompet? + Menyediakan pengesahan untuk membuat kunci + Kesalahan Impor, Coba impor ulang lagi. + Dompet %1$s sudah diimpor dengan kunci + Selamat datang di AlphaWallet + Kunci Seed Phrase + Tidak ada Kunci yang ditemukan + Transaksi Tertunda … + Skrip Lokal Tidak Diaktifkan + To use local TokenScripts you need to enable TokenScript override. Go to Settings, then click \'Enable TokenScript Override\'. + Terlalu banyak sidik jari yang gagal. Gunakan PIN atau tunggu 30 detik + Tidak ada sidik jari Terdaftar. Gunakan PIN + Tidak dapat memproses sidik jari. Gunakan PIN + Menerima + Kesalahan pada Elemen TokenScript + Kesalahan dalam elemen TokenScript: %1$s + masukan fungsi \'%1$s\' tidak ditemukan dalam elemen HTML. + Nilai yang dimasukkan tidak berlaku: %1$s + Nilai + Tidak dapat membuat kunci. Coba aktifkan keamanan pada ponsel Anda. + Sebuah Seed Phrase, seed pemulihan phrase atau seed phrase cadangan adalah daftar kata yang menyimpan semua informasi yang diperlukan untuk memulihkan dompet kripto. Perangkat lunak dompet biasanya akan menghasilkan seed phrase dan menginstruksikan pengguna untuk menuliskannya di atas kertas. + Transaksi Terlalu Besar + Jumlah transaksi terlalu besar + Tampilkan Kontrak Token + Rincian tanda tangan + Salin Alamat Kontrak + Diri sendiri + Gagal menandatangani pertukaran pesan + Tipe Token + Kecocokan TokenScript + Versi + TokenScript debug + tidak terpercaya + Tunjukkan Kode QR ke Gerai Penukaran: + Kelola Token + Terapkan Filter + Tidak Menampilkan Token Kosong + Saldo token untuk token ini kosong dan tidak akan ditampilkan kecuali Anda mengaktifkan saldo token kosong. Lanjutkan? + Nyalakan + Dompet tidak dapat membaca kunci terenkripsi; diperlukan impor ulang. + Otentikasi untuk membuka kunci tidak disediakan atau waktunya habis. Silakan coba lagi. + Sejak kunci ini dibuat, keamanan perangkat seluler telah berubah dan OS telah membatalkan kunci tersebut. Silakan masukkan kembali kunci Anda. + Pengesahan sidik jari gagal + BATALKAN + tersembunyi + Konfirmasi + Kode QR meminta Chain yang berbeda. Lanjutkan? + Permintaan Perubahan Chain + Transaksi Saldo Nol + Pengesahan + Favorit + Bantuan + Sistem + + + Tampilkan Alamat Dompet Saya + Ubah / Tambahkan Dompet + Buat cadangan dompet ini + Pemberitahuan + Kode sandi / ID Sentuh + Tingkat lanjut + Bantuan + Blog + TANYA JAWAB + Console + Hapus Perambah Cache + Muat Ulang Data Token + Ubah Bahasa + Tambahkan / Sembunyikan Token + Ubah Mata Uang + Pilih Mata Uang FIAT + Kesalahan pada berkas TokenScript + Browser cache dihapus + Data Token dihapus + Data token sedang dihapus … Mohon tunggu + Dompet Anda + Salin + Pembaruan Penting + Mulai Versi 3.0, AlphaWallet tidak mendukung perangkat yang berjalan di Android 6.0 dan di bawahnya. + Sembunyikan Pemberitahuan + %1$s tidak ditemukan + Pengelolaan TokenScript + Jaringan + + + Kecepatan (Gas) + Lambat + Cepat + Perkiraan Waktu Transaksi + 0 menit + Membuat Direktori AlphaWallet + Direktori AlphaWallet dibuat. Letakkan skrip ke dalam direktori ini untuk menggunakan skrip Anda. Perhatikan tokenscript.org untuk detail lebih lanjut tentang cara mudah membuat tampilan yang aman dan berguna untuk token Anda. + Sudah Menonton + ETerjadi kesalahan saat menandatangani transaksi. Silakan masukkan kembali kunci untuk dompet ini. + Halaman Tidak Masuk Daftar Putih + TokenScript mencoba mengarahkan ke halaman yang tidak masuk daftar putih %1$s. Jika Anda adalah pemilik situs ini, silakan kirimkan PR ke AlphaWallet repo untuk menambahkan halaman untuk daftar putih. + %1$s (%2$s) + Ketersediaan Fungsi + Parameter tidak valid saat melakukan Transaksi TokenScript . Silakan periksa masukan. + TokenScript diganti oleh%1$s%2$s\n\n + Token Asal:\n\n + (Debug) + Konfirmasi Penghapusan File + Hapus berkas %1$s + Hapus + Perbaharui TokenScripts + Peningkatan kecepatan + Batalkan Transaksi + 1 hari + %s hari + 1 jam + %s jam + 1 menit + %s menit + 1 kedua + %s detik + Tidak diketahui + Transaksi Tertunda untuk %s + Mengajukan Transaksi pengganti dengan harga gas yang lebih tinggi. + Batalkan Transaksi dengan mengganti pending dengan nilai TX nol . + Kesalahan TokenScript : %1$s\n\n + Arahkan kamera Anda pada kode QR + Terang + Kode QR saya + Jelajahi + Perkiraan Gas + Perkiraan Biaya Jaringan + Transaksi sudah ditulis ke blockchain, tidak dapat mengirim ulang atau membatalkan. + Transaksi Sudah Tertulis + Peringatan Kegagalan Transaksi + Node memprediksi transaksi ini akan gagal: %1$s + Gambar yang dipilih tidak mengandung data apa pun. + Token Ditampilkan + Token Tersembunyi + Abaikan + Mencari Token… + Sesi Berakhir + Kirim Transaksi ETH + Ditolak oleh pengguna + Putuskan Sesi Sambungan + Autentikasi gagal + Transaksi ditolak (coba gunakan lebih banyak gas) + dari: %1$s + dari + kepada: %1$s + disetujui untuk mentransfer token: %1$s + untuk dibelanjakan atas nama: %1$s + Kejadian yang tidak diketahui + Kirim + Diterima + Disetujui + Persetujuan diberikan + TokenScript Hubungi: + Hari ini + beralih ke Token + Sesi WalletConnect Tidak Berlaku + Sesi WalletConnect sebelumnya tidak diakhiri dengan benar. Silakan kembali ke halaman web dan putuskan sambungan, kemudian memulai sesi baru. + Data QR WalletConnect tidak berlaku + Waktu untuk koneksi habis. Harap batalkan permintaan dari Dapp dan mulai sesi baru. + Beban Pembayaran + Transaksi telah ditandatangani + Hapus catatan sesi ini? + Sesi telah dihentikan. Beralih kembali ke Dapp dan memulai sesi WalletConnect baru. + Transaksi tidak terkirim + Token yang Populer + Tidak dapat memperoleh kunci Biometrik + Semua Dana + Alamat Penerima + Konfirmasi Transaksi + Saldo + Rata-rata + Pesat + Cepat + Lambat + Khusus + Terlalu Rendah + Biaya Tinggi + Pengaturan Dapp + (Baru: %1$s %2$s) + Atur Kecepatan + Harga Gas: %1$s + Biaya Maksimum + Biaya Prioritas + Biaya Dasar: %1$s + Biaya didasarkan pada beban jaringan blockchain Ethereum saat ini. Mereka tidak bergantung pada AlphaWallet. AlphaWallet menggunakan oracle langsung %1$s dan memperbarui harga gas setiap 15 detik. + Tetapkan Milik Anda Sendiri + Nonce (pilihan) + Menghitung Batas Gas + Alamat Dompet + Transaksi dalam Proses + Transaksi dikirim ke blockchain Ethereum. Mungkin perlu beberapa menit untuk dikonfirmasi oleh penambang. + Tampilkan Rincian Transaksi + Kirim %1$s %2$s to %3$s + Menerima %1$s %2$s dari %3$s + Anda telah menerima persetujuan untuk memindahkan %1$s %2$s dari %3$s + Menyetujui %2$s untuk memindahkan %1$s + Panggilan kontrak + Gas tidak mencukupi + Kami memperkirakan transaksi ini akan gagal karena dana tidak mencukupi. Apakah Anda ingin mencoba mengirim? + Kirim Eth + Tukar Token + Penarikan + Setoran + Harga gas ditetapkan di bawah kecepatan \'lambat\'.\n Transaksi Anda mungkin membutuhkan waktu lama untuk ditulis atau mungkin tidak pernah ditulis. + Biaya Gas Mungkin Terlalu Rendah + Anda telah menetapkan biaya gas yang sangat tinggi. Tolong pastikan ini bukan kesalahan. AlphaWallet menggunakan gasnow.org live dan memperbarui harga gas setiap 15 detik. + Peringatan Biaya Tinggi! + Saldo Anda memiliki dana yang tidak mencukupi untuk nilai transaksi ditambah biaya transaksi. + Konversi ke xDAI + Kripto + Beli %1$s + Layar Penuh + Pilih Jaringan Browser Dapp baru + Browser terputus dari %1$s, silakan pilih jaringan baru untuk Browser + Apakah Anda ingin memutuskan sambungan dari Jaringan %1$s? + Anda terhubung ke Jaringan %1$s di Browser tetapi Anda baru saja menonaktifkan Jaringan %1$s di Jaringan Aktif. Apakah Anda ingin terhubung ke Jaringan %1$s? + Putuskan hubungan + Tetap Terhubung + Waspadalah terhadap penipu! Jangan membagikan Seed Phrase. + AlphaWallet TIDAK AKAN PERNAH bertanya tentang Seed Phrase Anda (terutama di Telegram). + Seed Phrase Anda (jangan bagikan dengan siapa pun) + OK, Sembunyikan Seed Phrase + Nama ENS tidak cocok dengan alamat dompet + Izinkan + Tolak + %s ingin menggunakan kamera Anda + Jaringan Utama + Apa itu Testnet? + Mengerti, aktifkan Testnet + Token Testnets seperti uang \'Monopoli\'. Mereka tidak memiliki nilai finansial, tetapi digunakan oleh para pengembang untuk mencoba desain baru tanpa perlu mengeluarkan koin yang berharga.. + Terhubung ke Jaringan + Pilih Pilihan Jaringan + Pilih setidaknya satu jaringan. + Portofolio + Masukkan Target Harga + Atur Peringatan Baru + Simpan Peringatan + Peringatan akan muncul di sini + Token + Beli ETH + Setel ulang semua data token yang terlampir (misalnya saldo yang tercatat, dll). AlphaWallet akan memuat ulang status dompet Anda saat ini. Tidak mempengaruhi kunci atau aset. + + Tambahkan Jaringan RPC Khusus + Nama Jaringan + URL RPC + Identitas Chain + Simbol + URL Penelusur Transaksi + Tambahkan Jaringan + Atur ulang + URL tidak berlaku + + Perhatikan bahwa mempercepat transaksi ini tidak menjamin transaksi asli Anda akan dipercepat. Jika upaya Anda berhasil, Anda akan dikenakan biaya penambang seperti di atas. + Perhatikan bahwa transaksi ini tidak menjamin transaksi awal Anda akan dibatalkan. Jika upaya Anda berhasil, Anda akan dikenakan biaya penambang seperti di atas. + Agar transaksi ini berhasil, harga gas harus setidaknya 10% lebih tinggi dari harga gas semula. + Pengaturan Kecepatan + Batalkan Pengaturan + Pilih + %s Terpilih + Situs ini meminta Anda untuk beralih ke Chain %1$s dengan IDENTITAS Chain: %2$s. Ini akan memuat ulang halaman. + Peringatan: Beralih ke jaringan Utama + Peringatan: Beralih ke Uji jaringan + WalletConnect: Jaringan tidak mendukung. Tutup koneksi dan ubah Jaringan DappBrowser + %1$s Aset | %2$s + %1$s Aset | %2$s + Ini adalah Testnet + Tukar dengan Quickswap + Beri Nama Dompet Ini + Masukkan Nama Dompet + Simpan Nama + Pilih Jumlah + Token yang Terpilih + Kirim Token + Jika Anda senang menggunakan %1$s, tolong bantu kami dengan memberi penilaian. + NANTI + TINGKAT + TIDAK, TERIMA KASIH + Tingkat %1$s + Terpasang: %1$s + Tersedia: %1$s + penjelajah API + Perbarui Jaringan + + Situs ini meminta Anda untuk mengaktifkan dan menambahkan Chain %1$s dengan IDENTITAS Chain: %2$s. Ini akan memuat ulang halaman + Tukar Permintaan Chain + Tambahkan Permintaan Chain + Tidak ada data grafik yang tersedia + %1$s (%2$s%%) Hari ini + Dompet + Rangkuman + Jaringan: %1$s + Tetapkan sebagai Halaman Utama + Jaringan + Tukar dengan 1 inci + Tukar + Kirim ke Alamat ini + Tetap mengikuti perkembangan terbaru + Kami mengirim email yang mengumumkan fitur-fitur utama. Apakah Anda ingin menerima email tersebut? (maksimal 1 email per minggu) + Alamat Email + Saya ingin menerima email seperti itu + Harap berikan alamat email yang berlaku + Github (Mengajukan permasalahan) + Chains untuk memindai + Lihat + Identitas Chain sudah digunakan + Status + Terhubung ke + Tidak Terhubung + Online + %1$s (%2$s) Hari ini + Kinerja + Cari token + Aset + Tata Kelola Pemerintahan + Barang Koleksi + Apa yang baru? + Di bawah + Di atas + Tambahkan Peringatan Harga Baru + Harga Saat Ini: + Informasi + Aktivitas + Peringatan + Jaringan Tidak Diketahui + Dapp ini meminta untuk berubah ke Chain yang tidak diketahui: %1$s + + Tidak ada sesi Aktif + Hubungkan Dompet + Seed phrase hanya bisa berisi kata-kata + Tautan Eksternal + Penjelasan + Rincian + Mode Gelap + Kirim Beberapa Token + Beralih ke Tampilan Grid + Beralih ke Tampilan Daftar + Muat ulang Metadata + Harga Pasar: + Saya sudah memiliki Dompet + Pilih Mode + Info Jaringan + Situs web + Terang + Gelap + Otomatis + Unik + %1$s%% memiliki ciri ini + Gunakan 1559 Transaksi + Percobaan 1559 Transaksi + IDENTITAS Chain: %1d + Dibuat oleh + Standar Token + Jumlah Pasokan + Pemilik + Pemilik + Penjualan Terakhir + Tidak dapat meningkatkan kunci: Aktifkan kunci layar pada ponsel + Tidak dapat meningkatkan kunci: %1$s + Tidak ditemukan Kunci + Tidak dapat menyimpan kunci: %1$s + Peringatan Pencarian ENS + AlphaWallet telah mendeteksi perbedaan antara penanda waktu ethereum dan waktu perangkat Anda saat ini, atau mungkin koneksi blockchain tidak disinkronkan, harap periksa waktu mobile Anda sudah benar, atau abaikan peringatan ini. + Penurunan + Peningkatan + Transaksi telah habis waktunya. Anda bisa melihat status di halaman aktivitas. + Batas Waktu Transaksi + Slippage (adalah perbedaan harga yang dapat terjadi antara saat order trading berlalu dan eksekusi aktualnya) + Buka Pengaturan + Tidak ada koneksi yang ditemukan untuk Chain ini. + Biaya + Harga Terkini + Minimum Diterima + Saldo: + Status Node + Saldo %1$s tidak mencukupi + Pilih Token + Tidak ada hasil yang ditemukan + Konfirmasi Transaksi + Pengambilan Chains + Pengambilan koneksi + Pengambilan penawaran + Bantuan + Apa yang dimaksud dengan status Node? + Dengan Node Status, Anda dapat mengukur seberapa cepat node merespon transaksi + di bawah 1 detik + lebih dari 1 detik + tidak merespon + Mode Testnet + Di mana Token saya? + Jangan khawatir. Token Anda aman. Anda sedang melihat jaringan Testnet. Mereka digunakan oleh para pengembang untuk mencoba desain baru. Anda bisa beralih ke jaringan utama kapan saja. + Beralih ke Mainnet + Pilih Token + Khusus + Nama + Catatan + Terhubung + Terhubung ke + Tanda tangan + Ini akan menginformasikan situs jarak jauh alamat dompet Anda adalah %s + Harga Dasar + Harga Rata-rata + Penyedia layanan + Hapus Kosong + Hapus Semua + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + Transfer yang aman + Kebijakan pribadi + Ketentuan Layanan + Dompet yang terhubung + Keanehan + Pertukaran Pilihan + Pertukaran Pilihan: %s + Mengambil Rute + Pilih Rute + Pilih Pertukaran + Rute Baru di %s + Jumlah Untuk Tukar + Provider Website + Detail Kutipan + Tukar melalui %s + Tidak ada rute yang ditemukan untuk parameter yang diberikan. + Pilih setidaknya satu bursa. + Tidak digunakan lagi + Tidak dapat melakukan tindakan pada dompet khusus jam tangan ini. + Pemindaian sedang berlangsung: Harap tunggu hingga selesai + Aktifkan rantai untuk token yang dipilih? + (Tanpa judul) + Detail Sesi + Proposal Sesi + Dompet berbeda dengan dompet aktif. + Analitik + Pelaporan Kerusakan + Bantu kami meningkatkan AlphaWallet dengan membagikan data anonim Anda kepada kami. Ini tidak termasuk informasi keuangan apa pun. + Bagikan Data Anonim? + Aktifkan rantai yang diperlukan? + Token yang dipilih ada di rantai saat ini tidak dipilih, mengaktifkan rantai yang tidak dipilih? + diff --git a/app/src/main/res/values-land/strings.xml b/app/src/main/res/values-land/strings.xml index 76ff0455f3..1f20a793a3 100644 --- a/app/src/main/res/values-land/strings.xml +++ b/app/src/main/res/values-land/strings.xml @@ -3,5 +3,11 @@ Send Multiple Tokens QR scanning requires Android 7.0 (API level 24) or above. Reset + Active connections to browser-based Dapp + User disconnect the session. + Name + Wallet + Wallet not exist or it\'s watch only. + Chain %s is not supported. Testnet mode - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index accc52cd4f..be646101ab 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -24,7 +24,6 @@ မလုပ်ရသေးပါ။ ပြန်လုပ်မည်။ %s ပြောင်းရွှေ့မည် %s ဝယ်မည် - ပြန်လုပ်မည် ဝေါလက် ရှိပြီးဖြစ်ပါသလား။ ဝေါလက် အသစ်တစ်ခုဖန်တီးမည် @@ -77,8 +76,6 @@ ပို၍အသေးစိတ်ပြုလုပ်ရန် ဝေါလက် ထည့်မည် Ethersacan တွင်ကြည့်မည် - Coinbase - LocalEthereum Changelly (ခရစ်ဒစ်ကတ်ဖြင့်ဝယ်မည်) @@ -94,8 +91,6 @@ မရှိပါ ဝေါလက်ကိုမရွေးချယ်ရသေးပါ ဤနေရာတွင်လုပ်ဆောင်မှုများကိုပြထားမည်ဖြစ်သည် - -- - -- ပို့နေပါသည်… လိပ်စာကို QRကုတ်မှတဆင့်ရယူမည် လုပ်ငန်းဆောင်ရွက်ခ ကြီးမြင့်လာသည်နှင့်အမျှ လုပ်ငန်းစဥ်ဆောင်ရွက်ခပို၍ဈေးကြီးလာမည်ဖြစ်သည်။ သို့သော်လည်း Ethereum ကွန်ရက် ပေါ်ရှိသင်၏လုပ်ငန်းစဥ်ကိုပို၍မြန်ဆန်စွာပြီးမြောက်စေမည်ဖြစ်သည်။ @@ -108,7 +103,6 @@ တိုကင်စာရင်းပြလုပ်ခြင်းပျက်ပြယ်ပါသည် ဝေါလက် အသစ်ပြုလုပ်ခြင်းပျက်ပြယ်ပါသည် - AlphaWallet အားလုံး ငွေကြေးများ အသေးစိတ်အဆင့်မြှင့်တင်ခြင်း @@ -127,10 +121,8 @@ ထုတ်ယူမည် ထုတ်ယူပြီးလက်မှတ်များ တိုကင်ကိုအသုံးပြုမည် - %1$s%2$s - %1$s (%2$s) အရောင်းစာရင်းပြုလုပ်မည် %1$s ကိုရောင်းရန်ရွေးချယ်သည်: ထုတ်ယူမည့်လက်မှတ်များကိုရွေးချယ်ပါshow @@ -196,11 +188,9 @@ ရက်/လ/နှစ် - ETH လက်မှတ်အရေအတွက် စုစုပေါင်းကုန်ကျငွေ: USDဖြင့် - %1$s %2$s %1$s ပမာဏာကိုရွေးချယ်ပါ သင်၏လက်မှတ်ကိုထုတ်ယူပြီးပါပြီ ရှာရန် @@ -213,11 +203,7 @@ Seed Phrase ဆိုသည်မှာအဘယ်နည်း။ ETHကိုမိမိ ဝေါလက်အတွင်းသို့မည်ကဲ့သို့လွှဲပြောင်းရမည်နည်း။ Tokenscriptဆိုသည်မှာအဘယ်နည်း။ - ကိုယ်ရေးအချက်အလက်ဆိုင်ရာမူဝါဒ - ဝန်ဆောင်မှုစည်းမျဥ်းများ - ကိုယ်ရေးအချက်အလက်ဆိုင်ရာမူဝါဒ.html - ဝန်ဆောင်မှုစည်းမျဥ်းများ.html - tokenscript_explaination.html + ဝန်ဆောင်မှု၏စည်းကမ်းချက်များ ၂၄နာရီ ကျေးဇူးတင်ရှိခြင်း %1$s/လက်မှတ် @@ -260,7 +246,6 @@ MagicLinkကိုဝေမျှမည် MagicLinkကိုထုတ်ပေးပါ ဈေးကွက်တွင်စာရင်းတင်မည် - --:-- MagicLink သက်တမ်းကုန်ဆုံးချိန် သက်တမ်းကုန်ဆုံးချိန်ကိုရွေးပါ သက်တမ်းကုန်ဆုံးမည့်ရက်ကိုရွေးပါ @@ -284,12 +269,6 @@ အရေးကြီးသောအရာများ 0.00 ETH ဝေါလက်ပြောင်းမည် - Facebook - Twitter - ERC 20 - ERC 875 - ERC 875* - ERC 1155 Ethereum သည် အများသူငှာပါဝင်နိုင်သော open-source နှင့် Blockchain အခြေခံ နည်းပညာပလက်ဖောင်းတစ်ခုဖြစ်ပါသည်။ Ethereum ပေါ်တွင် smart contract ဖြင့်လုပ်ငန်းများကိုလည်ပတ်နိုင်သောစနစ်တစ်ခုပါဝင်ပါသည်။ ၄င်းသည် blockchain နည်းပညာထုတ်ကုန်များနှင့် ဝန်ဆောင်မှုများကိုအတွက်အခြေခံကျသောအဆောက်အဦးတစ်ခုလည်းဖြစ်ပါသည်။ \n\nETH/Ether သည် Ethereum ပလက်ဖောင်းပေါ်ရှိ Blockchain ပေါ်တွင်လည်ပတ်နေသော ခရစ်ပ်တိုငွေကြေးတစ်မျိုးဖြစ်ပါသည်။ Ether ကိုအကောင့်အချင်းချင်းလွှဲပြောင်းပေးပို့မှုများပြုလုပ်နိုင်သလို သက်ဆိုင်ရာNodeများရှိmining လုပ်ငန်းများတွင်ပါဝင်ကူညီတွက်ချက်ပေးခြင်းဖြင့်လည်း ဆုကြေးအဖြစ်ရရှိနိုင်ပါသည်။ Ethereum သည် EVM(Ethereum Virtual Machine) ဟုခေါ်သော လုံးဝဗဟိုချုပ်ကိုင်မှုလုံးဝမရှိသော Turing-virtual machine ကိုထောက်ပံ့ကူညီပေး၍ နိုင်ငံတကာကွန်ရက်ပေါ်ရှိNodeများတွင်scripts များကိုလည်ပတ်ဆောက်ရွက်စေပါသည်။ Gas ဆိုသည်မှာ blockchain ပေါ်တွင်အရောင်းအဝယ်များပြုလုပ်ရန်အတွက်ကုန်ကျစားရိတ်ကိုသတ်မှတ်ပေးသာစနစ်ဖြစ်ပါသည်။ Gasကို network ပေါ်ရှိအရင်းအမြစ်များကို ခွဲဝေနေရာချထားရန်နှင့်အသုံးမဝင်သောအရာများကိုလျှော့ချရန်အတွက်အသုံးပြုပါသည်။ လက်ရှိတွင် Ethereum သည် smart contract အပလီကေးရှင်းများနှင့် ဝန်ဆောင်မှုများကိုပြုလုပ်ရန်အတွက် အကောင်းဆုံးနှင့်အကြီးမားဆုံးသောBlockchain ဖြစ်ပါသည်။ Alphawallet သည် Ethereum Blockchain ကိုစတင်ပံ့ပိုးနေပြီး ၄င်း၏နည်းပညာများကို တိုးတက်အောင်ပြုလုပ်နေပါသည်။ သင်၏Ethereumများကို ငွေကြေးအဖြစ်သို့ပြောင်းလဲနိုင်ပြီး ခရစ်ပ်တိုငွေကြေးလဲလှယ်သည့်ဝန်ဆောင်မှုကုမ္ပဏီများမှတဆင့်သင်၏ဘဏ်အကောင့်သို့ငွေကြေးများကိုပြောင်းရွှေ့နိုင်ပါသည်။ @@ -336,8 +315,6 @@ MagicLinkကိုဝယ်ယူသည် ထုတ်လုပ်သည် ဖျက်ဆီးသည် - ပြန်လည်ရောနှောသည် - NFTပြုလုပ်သည် ရှေ့သို့သွားမည် သိမ်းဆည်းမည် @@ -407,14 +384,12 @@ ငွေကြေးတန်ဖိုးကျနေချိန်တွင်ယူမည် ငွေကြေးများကိုထည့်သွင်းမည် သင်လက်ခံရရှိလိမ့်မည် - QR ကုတ်ကိုဖတ်မည် စကင်ဖတ်ရာတွင်ရလာဒ်များမမှန်ကန်ပါ Ethereum လိပ်စာ Ethereum လိပ်စာကိုဖတ်ပြီးပါပြီ။ တိုကင်အဖြစ်ထည့်သွင်းရန်ကြိုးစားကြည့်ပါမည်လား။ တိုကင်ကိုဖော်ပြမည် %1$s Blockchain - 0.00 %s Telegram အပြောင်းအရွှေ့တောင်းဆိုမှု တိုကင်အပြောင်းအရွှေ့တောင်းဆိုမှု: %1$s @@ -449,6 +424,7 @@ ဝက်ဘ်လိပ်စာကိုရိုက်ထည့်ရှာဖွေပါ ကွန်ရက် အသက်ဝင်နေသောကွန်ရက်များကိုရွေးချယ်ပါ + ကွန်ရက်များကို ဖွင့်ထားသည်။ (%s) စုဆောင်ထားသောအရများ တိုကင်လုပ်ငန်းစဥ်များ Alphawallet အသိပေးစာလမ်းကြောင်းများ @@ -556,7 +532,6 @@ function input \'%1$s\' not found in HTML elements. Invalid input value: %1$s Value - LinkedIn သော့ကိုဖန်တီး၍မရပါ။ သင်၏ဖုန်းတွင်လုံခြုုံရေးခွင့်ပြုချက်ပြုလုပ်ပေးပါ။ Recovery Phrase(သို့) backup Seed Phrase သိမ်းဆည်းခြင်းဆိုသည်မှာ ခရစ်ပ်တိုဝေါလက်တစ်ခုကိုပြန်လည်ရယူရန်အတွက် လိုအပ်သောစကားလုံးများကိုသိမ်းဆည်းသော လုပ်ငန်းစဥ်ဖြစ်ပါသည်။ အသုံးပြုသူများသည်ပုံမှန်အားဖြင့်ဝေါလက်အသုံးပြုရန် seed phrase ကိုထည့်သွင်းရမည်ဖြစ်ပြီး ထို Seed Phrase ကို စာရွက်ပေါ်တွင် မှတ်သားထားရန်ညွှန်ကြားထားလေ့ရှိသည်။ လုပ်ငန်းစဥ်ကြီးမားလွန်းနေသည်။ @@ -565,10 +540,8 @@ လက်မှတ်အသေးစိတ် စာချုပ်လက်မှတ်ကိုကူးယူမည် Self - ERC721T ထုတ်ယူရန်မက်ဆေ့ကိုလက်မှတ်ထိုးခြင်းမအောင်မြင်ပါ။ တိုကင်အမျိုးအစား - Reddit CHANGE: TokenScript Compatibiity TokenScript debug မယုံကြည်ရပါ @@ -606,16 +579,14 @@ ထိန်းချုပ်ခန်း Browser Cacheဖိုင်များကိုရှင်းလင်းမည် တိုကင်အချက်အလက်များကိုပြန်လည်ဖော်ပြပါ - TokenScript ဘာသာစကားပြောင်းမည် တိုကင်များကိုပုန်းကွယ်မည်/ထည့်မည် - Instagram ငွေကြေးအမျိုးအစားပြောင်းမည် တရားဝင်ငွေကြေးအမျိုးအစားကိုရွေးချယ်ပါ TokenScript ဖိုင်အမှားအယွင်းဖြစ်နေပါသည် Browser cache ဖိုင်များကိုရှင်းလင်းမည် တိုကင်အချက်အလက်များကိုရှင်းလင်းပြီးပါပြီ - ချိတ်ဆက်ထားသောဝေါလက်များ + ချိတ်ဆက်ထားသောဝေါလက်များ တိုကင်အချက်လက်များကိုရှင်းလင်းနေပါသည်။ ခေတ္တစောင့်ဆိုင်းပါ။ သင်၏ ဝေါလက်များ ကူးယူမည် @@ -643,7 +614,6 @@ Invalid parameter while calling TokenScript Transaction. Please check inputs. TokenScript overridden by %1$s%2$s\n\n မူလတိုကင်များ:\n\n - %1$s -> %2$s\n\n (Debug) ဖိုင်ဖျက်ရန်အတည်ပြုသည် %1$sဖိုင်ကိုဖျက်မည် @@ -682,7 +652,6 @@ ပုန်းကွယ်ထားသောတိုကင်များ ချန်လှပ်သည် Tokens များကိုရှာရန်.... - WalletConnect လုပ်ငန်းစဥ်ရပ်နားမည် ETH လုပ်ငန်းစဥ်ကိုပေးပို့သည် အသုံးပြုသူမှငြင်းပယ်သည် @@ -691,7 +660,6 @@ လုပ်ငန်းစဥ်ငြင်းပယ်သည် (လုပ်ငန်းဆောင်ရွက်ခ များပိုမိုအသုံးပြု၍ထပ်မံကြိုးစားကြည့်ပါ) မှ: %1$s မှ - %1$s %2$s to: %1$s တိုကင်များကိုပြောင်းရွှေ့ရန်ခွင့်ပြုသည်: %1$s %1$sကိုယ်စားသုံးစွဲရန် @@ -701,8 +669,6 @@ ခွင့်ပြုသည် ခွင့်ပြုချက်ပေးခဲ့ပါသည် TokenScript call: - စမ်းသပ်ကွန်ရက် - ယနေ့ @@ -711,10 +677,8 @@ WalletConnect နှင့်ချိတ်ဆက်ထားသောလုပ်ငန်းစဥ်ကို မှန်ကန်စွာမပိတ်ရသေးပါ။ ကျေးဇူးပြု၍ အင်တာနက်စာမျက်နှာကိုပြန်သွား၍ ချိတ်ဆက်ထားမှုကိုဖြုတ်ပါ။ ထို့နောက် ချိတ်ထားမှုလုပ်ငန်းစဥ်အသစ်ကိုပြန်စပါ။ WalletConnectမှပြထားသောQRကုတ်သည်အသုံးပြု၍မရပါ Connection attempt timed out. Please cancel the request from the Dapp and start a new session. - Nonce ဝန်ချိန် လုပ်ငန်းစဥ်ကိုလက်မှတ်ထိုးပြီးပါပြီ - %1$s ယခုလုပ်ငန်းစဥ်မှတ်တမ်းကိုဖျက်ပစ်ပါမည်လား။ လုပ်ငန်းစဥ်ကိုပိတ်ချပြီးပါပြီ။ Dappကိုပြန်သွား၍ WalletConnectလုပ်ငန်းစဥ်ကိုအသစ်ပြန်စပါ။ လုပ်ငန်းစဥ်မပို့ခဲ့ပါ @@ -828,13 +792,11 @@ %1$s Assets | %2$s စမ်းသပ်ကွန်ရက်သို့ရောက်ရှိနေပါသည် Quickswapဖြင့် ပြောင်းလဲမည် - Email - Discord ယခုဝေါလက်ကိုအမည်ပေးပါ ဝေါလက်အမည်ကိုထည့်သွင်းပါ အမည်ကိုသိမ်းဆည်းမည် ပမာဏကိုရွေးချယ်ပါ - လုံခြုံစွာပြောင်းရွှေ့မည် + လုံခြုံစွာပြောင်းရွှေ့မည် ရွေးချယ်ထားသောတိုကင်များ တိုကင်များကိုပို့မည် အသုံးပြုသည်ကိုကျေနပ်မှုရှိလျှင် ကျေးဇူးပြု၍ ကျွန်တော်တို့အားအဆင့်သတ်မှတ်ပေး၍ကူညီပါ။ @@ -878,7 +840,6 @@ တိုကင်နာမည် ဗားရှင်း မျိုးစေ့ထားသောစကားစုများတွင်စကားလုံးများသာပါ 0 င်နိုင်သည် - Token # External Link Description Details @@ -891,11 +852,10 @@ Select Mode Network Info Assets - DeFi Governance Collectibles Connect Wallet - No Active sessions + အသက်ဝင်သော ဆက်ရှင်များမရှိပါ။ Send Multiple Tokens This dapp is requesting to change to an unknown chain: %1$s @@ -918,6 +878,12 @@ Use 1559 Transactions Experimental 1559 Transactions ပြန်လည်သတ်မှတ်ပါ။ + နည်းလမ်းများ + ဘရောက်ဆာအခြေခံ Dapp သို့ ချိတ်ဆက်မှုများ + အသုံးပြုသူသည် စက်ရှင်ကို ချိတ်ဆက်မှုဖြုတ်သည်။. + နာမည် + ပိုက်ဆံအိတ် + ပိုက်ဆံအိတ် မရှိပါ သို့မဟုတ် ၎င်းသည် နာရီသာဖြစ်သည် Chain ID: %1d Created By Token Standard @@ -931,7 +897,6 @@ Unable to store key: %1$s ENS Lookup Warning AlphaWallet has detected a discrepancy between ethereum\'s timestamp and your device\'s current time, or possibly the blockchain connection is not synchronized, please check your phone\'s time is correct, or ignore this warning. - 0 ရုပ်ရည် တိုးမြှင့်လာသည် The transaction has timed out. You can view the status in activity page. @@ -956,19 +921,70 @@ under 1 second over 1 second not responding + ကွင်းဆက် %s ကို မပံ့ပိုးပါ။ + မချိတ်ဆက်မီ ကွန်ရက် %s ကို ဖွင့်ထားရပါမည်။ + Select Token + Custom + WalletConnect သည် အသက်ဝင်ပါသည်။ + အသက်ဝင်သော ဆက်ရှင်များကို ကြည့်ရန် နှိပ်ပါ။ Testnet မုဒ် ငါ့တိုကင်များ ဘယ်မှာလဲ စိတ်မပူပါနှင့်။ သင့်တိုကင်များသည် ဘေးကင်းပါသည်။ သင်သည် Testnet ကွန်ရက်များကို ကြည့်ရှုနေပါသည်။ ဒီဇိုင်းအသစ်များကို စမ်းသပ်ရန်အတွက် ၎င်းတို့ကို developer များက အသုံးပြုကြသည်။ သင်သည် အချိန်မရွေး Mainnet သို့ ပြောင်းနိုင်သည်။ Mainnet သို့ပြောင်းပါ။ - Select Token - Custom Name Note - URL Connect Connect to Sign This will inform the remote site your wallet address is %s Floor Price Average Price + Provider + Delete Empty + Delete All + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + ဒီပိုက်ဆံအိတ်အမည်ပြောင်းပါ + %1$s တိုကင် + %1$s ချိန်ခွင်လျှာမလုံလောက် + ကိုယ်ရေးအချက်အလက်မူဝါဒ + ရှားပါးသည်။ + နှစ်သက်သော ဖလှယ်မှုများ + ဓာတ်ငွေ့ကြေး: %s + လမ်းကြောင်းများ ရယူခြင်း။ + လမ်းကြောင်းကို ရွေးပါ။ + Exchanges ကို ရွေးပါ။ + လမ်းကြောင်းအသစ်များ %s + လဲလှယ်ရန် ပမာဏ + ဝန်ဆောင်မှုပေးသော ဝဘ်ဆိုဒ် + ကိုးကားအသေးစိတ် + မှတဆင့်လဲလှယ်ပါ။ %s + ပေးထားသော ကန့်သတ်ဘောင်များအတွက် လမ်းကြောင်းများ ရှာမတွေ့ပါ။ + အနည်းဆုံးလဲလှယ်မှုတစ်ခုကို ရွေးပါ။ + ကန့်ကွက်ထားသည်။ + ဤ Watch-only ပိုက်ဆံအိတ်တွင် လုပ်ဆောင်ချက်ကို မလုပ်ဆောင်နိုင်ပါ။ + စကင်န်လုပ်နေသည်- ပြီးစီးရန်စောင့်ဆိုင်းပါ။ + ရွေးချယ်ထားသော တိုကင်များအတွက် ကွင်းဆက်များကို ဖွင့်မလား။ + (ခေါင်းစဉ်မရှိ။) + အပိုင်းအသေးစိတ် + Session အဆိုပြုချက် + ပိုက်ဆံအိတ်သည် တက်ကြွသောပိုက်ဆံအိတ်နှင့် ကွဲပြားသည်။ + ပိုင်းခြားစိတ်ဖြာပါ။ + ပျက်စီးမှုသတင်းပို့ခြင်း။ + သင်၏အမည်မသိဒေတာကို ကျွန်ုပ်တို့နှင့်မျှဝေခြင်းဖြင့် AlphaWallet တိုးတက်စေရန် ကျွန်ုပ်တို့ကိုကူညီပါ။ ၎င်းတွင် မည်သည့်ဘဏ္ဍာရေးဆိုင်ရာ အချက်အလက်မှ မပါဝင်ပါ။ + အမည်မသိဒေတာကို မျှဝေမလား။ + လိုအပ်တဲ့ သံကြိုးတွေကို လုပ်ဆောင်နိုင်မလား။ + ရွေးချယ်ထားသော အမှတ်အသားများသည် လောလောဆယ် ရွေးချယ်မထားသော သံကြိုးများပေါ်တွင် ရွေးချယ်ထားပြီး ရွေးချယ်မှုမရှိသော သံကြိုးများ ဖြစ်စေနိုင်ပါသလော။ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 2a37abfac5..076e442d94 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -22,6 +22,7 @@ @color/cod @color/dusty @color/cod + @color/mine @color/translucent_dark @color/mercury diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 62a40f251f..49daf937d7 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -22,7 +22,6 @@ Không, quay lại Chuyển khoản %s Mua %s - Thử lại Đã có ví? Tạo một ví mới @@ -75,8 +74,6 @@ Chi tiết Thêm ví Xem trên Block Explorer - Coinbase - LocalEthereum Changelly (Mua với thẻ tín dụng) Chi tiết giao dịch @@ -91,8 +88,6 @@ N/A Ví chưa được chọn Các hoạt động sẽ xuất hiện tại đây - -- - -- Đang gửi… Nhận địa chỉ từ Mã QR The higher the gas price, the more expensive your transaction fee will be, but the quicker your transaction will be processed by the Ethereum network. @@ -105,12 +100,11 @@ Không tải được danh sách tiền Không tạo được ví - AlphaWallet Tất cả Tài sản Tùy chọn nâng cao Sao chép địa chỉ ví - Đổi tên Ví này + Đổi tên ví này Tên Venue Ngày @@ -123,9 +117,7 @@ Redeem Tickets Redeemed Use Token - %1$s%2$s - %1$s (%2$s) Create Sales Order Chọn %1$s để bán: Select Tickets to Redeem:show @@ -190,11 +182,9 @@ DD/MM/YY - ETH Quantity of Tickets Total Cost: Equivalent in USD - %1$s %2$s Select Quantity of %1$s Your ticket has been redeemed Tìm kiếm cho: @@ -207,11 +197,8 @@ Cụm từ bí mật là gì? Làm cách nào để chuyển ETH vào ví của tôi? TokenScript là gì? - Chính sách bảo mật - Điều khoản dịch vụ - privacyPolicy.html - termsOfService.html - tokenscript_explaination.html + Chính sách bảo mật + Điều khoản dịch vụ 24 hours Appreciation %1$s/Ticket @@ -254,7 +241,6 @@ Share MagicLink Generate MagicLink Danh sách trên thị trường - --:-- MagicLink Expiry Time Vui lòng chọn thời gian hết hạn Vui lòng chọn ngày hết hạn @@ -278,11 +264,6 @@ Quan trọng 0.00 ETH Thay đổi ví - Facebook - Twitter - ERC 20 - ERC 875 - ERC 875* Ethereum is an open-source, public, blockchain-based distributed computing platform and operating system featuring smart contract (scripting) functionality. It is the infrastructure for blockchain products and services.\n\nETH/Ether is a cryptocurrency whose blockchain is generated by the Ethereum platform. Ether can be transferred between accounts and used to compensate participant mining nodes for computations performed. Ethereum provides a decentralised Turing-complete virtual machine, the Ethereum Virtual Machine (EVM), which can execute scripts using an international network of public nodes. "Gas", an internal transaction pricing mechanism, is used to mitigate spam and allocate resources on the network. Currently, Ethereum is the best and largest blockchain for developing smart contract application and services. AlphaWallet starts with supporting Ethereum blockchain and helps improving its technology. You can convert Ethereum to fiat money and transfer it to your bank account through any cryptocurrency exchange. @@ -329,8 +310,6 @@ Magiclink Purchase Mint Burn - Remix - Commit NFT Tiếp tục Lưu lại @@ -400,14 +379,12 @@ Take Currency Drop Currency Import bạn sẽ nhận - Quét mã QR Kết quả quét không hợp lệ Địa chỉ Ethereum Just scanned an Ethereum address. Do you want to try to load this as a Token? Nạp Token %1$s Blockchain - 0.00 %s Telegram (Hỗ trợ khách hàng) Yêu cầu chuyển Token transfer request: %1$s @@ -442,6 +419,7 @@ Tìm kiếm hoặc nhập địa chỉ web Networks Chọn mạng đang hoạt động + Mạng được kích hoạt (%s) Bộ sưu tầm Token Function Kênh thông báo Alphawallet @@ -550,7 +528,6 @@ function input \'%1$s\' not found in HTML elements. Giá trị đầu vào không hợp lệ: %1$s Value - LinkedIn Không thể tạo khóa. Thử bật bảo mật trên điện thoại của bạn. Cụm từ bí mật, cụm từ khôi phục bí mật hoặc cụm từ bí mật dự phòng là danh sách các từ lưu trữ tất cả thông tin cần thiết để khôi phục ví tiền điện tử. Phần mềm Wallet thường sẽ tạo ra một cụm từ bí mật và hướng dẫn người dùng viết nó ra giấy. Giao dịch quá lớn @@ -559,10 +536,8 @@ Chi tiết chữ ký Sao chép địa chỉ hợp đồng Self - ERC721T Failed to sign redeem message Kiểu Token - Reddit TokenScript Compatibiity Phiên bản TokenScript debug @@ -601,10 +576,8 @@ Bảng điều khiển Xóa bộ nhớ đệm của trình duyệt Tải lại dữ liệu tiền mã hóa - TokenScript Thay đổi ngôn ngữ Thêm / Ẩn Token - Instagram Thay đổi tiền tệ Chọn tiền tệ pháp định TokenScript file error @@ -637,7 +610,6 @@ Tham số không hợp lệ khi gọi Giao dịch TokenScript. Vui lòng kiểm tra đầu vào. TokenScript bị ghi đè bởi %1$s%2$s\n\n Origin Tokens:\n\n - %1$s -> %2$s\n\n (Debug) Xác nhận Xóa tệp Xóa tệp %1$s @@ -673,7 +645,6 @@ Ẩn Tokens Bỏ qua Tìm kiếm Tokens... - WalletConnect Kết thúc kết nối Send ETH Transaction Người dùng từ chối @@ -682,7 +653,6 @@ Giao dịch bị từ chối (thử sử dụng thêm gas) từ: %1$s từ - %1$s %2$s tới: %1$s được chấp thuận để chuyển các Token: %1$s to spend on behalf of: %1$s @@ -692,17 +662,14 @@ Chấp thuận Approval granted TokenScript call: - Mạng thử nghiệm Hôm nay Đi tới Token WalletConnect không hợp lệ Previous WalletConnect session not correctly terminated. Please return to webpage and disconnect, then start a new session. Dữ liệu QR WalletConnect không hợp lệ Connection attempt timed out. Please cancel the request from the Dapp and start a new session. - Nonce Payload Giao dịch đã ký - %1$s Delete this session record? Session has been terminated. Switch back to Dapp and start a new WalletConnect session. Transaction not sent @@ -807,8 +774,6 @@ Cảnh báo: Đang chuyển qua mạng thử nghiệm WalletConnect: Mạng không được hỗ trợ. Đóng kết nối và thay đổi mạng bằng trình duyệt Dapp Hoán đổi bằng Quickswap - Email - Discord Đặt tên cho ví này Nhập Tên Ví Lưu lại @@ -855,7 +820,6 @@ Performance Search token Assets - DeFi Quản trị Sưu tầm Có gì mới? @@ -869,10 +833,9 @@ Unkown Network This dapp is requesting to change to an unknown chain: %1$s - No Active sessions + Không có phiên hoạt động nào Connect Wallet Cụm từ hạt giống chỉ có thể chứa từ - Token # External Link Description Details @@ -895,6 +858,12 @@ Use 1559 Transactions Experimental 1559 Transactions Cài lại + Phương pháp + Kết nối đang hoạt động với Dapp dựa trên trình duyệt + Người dùng ngắt kết nối phiên. + Tên + Ví tiền + Ví không tồn tại hoặc đó chỉ là đồng hồ Chain ID: %1d Created By Token Standard @@ -908,7 +877,6 @@ Unable to store key: %1$s ENS Lookup Warning AlphaWallet has detected a discrepancy between ethereum\'s timestamp and your device\'s current time, or possibly the blockchain connection is not synchronized, please check your phone\'s time is correct, or ignore this warning. - 0 Giảm bớt Tăng lên The transaction has timed out. You can view the status in activity page. @@ -933,19 +901,69 @@ under 1 second over 1 second not responding + Chuỗi% s không được hỗ trợ. + Mạng% s phải được bật trước khi kết nối. + Select Token + Custom + WalletConnect đang hoạt động + Nhấp để xem các phiên hoạt động Chế độ testnet Token của tôi ở đâu? Đừng lo. Các mã thông báo của bạn được an toàn. Bạn đang xem các mạng Testnet. Chúng được các nhà phát triển sử dụng để thử các thiết kế mới. Bạn có thể chuyển sang Mainnet bất kỳ lúc nào. Chuyển sang Mainnet - Select Token - Custom Name Note - URL Connect Connect to Sign This will inform the remote site your wallet address is %s Floor Price Average Price + Provider + Delete Empty + Delete All + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + Không đủ %1$s cân bằng + Chuyển giao an toàn + Ví kết nối + Việc hiếm có + Sở giao dịch ưu tiên + Phí xăng: %s + Tìm nạp các tuyến đường + Chọn tuyến đường + Chọn trao đổi + Các tuyến đường mới trong %s + Số tiền để hoán đổi + Trang web của nhà cung cấp + Trích dẫn Chi tiết + Hoán đổi qua %s + Không tìm thấy các tuyến đường cho các thông số đã cho. + Chọn ít nhất một sàn giao dịch. + Không được chấp nhận + Không thể thực hiện hành động trên ví Chỉ xem này. + Đang quét: Vui lòng chờ hoàn thành + Kích hoạt chuỗi cho mã thông báo đã chọn? + (Không tiêu đề) + Session Details + Chi tiết phiên + Ví khác với ví đang hoạt động. + Phân tích + Báo cáo sự cố + Hãy giúp chúng tôi cải thiện AlphaWallet bằng cách chia sẻ dữ liệu ẩn danh của bạn với chúng tôi. Điều này không bao gồm bất kỳ thông tin tài chính. + Chia sẻ dữ liệu ẩn danh? + Kích hoạt chuỗi yêu cầu? + Các mã thông báo đã chọn nằm trên các chuỗi hiện không được chọn, cho phép các chuỗi chưa được chọn? diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index b1e6d66468..3120d6e296 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -21,7 +21,6 @@ 否,重复 转移%s 购买%s - 重试 已经有钱包了吗? 创建新钱包 @@ -68,8 +67,6 @@ 更多详情 添加钱包 在Block Explorer上查看 - Coinbase - LocalEthereum Changelly 交易详情 @@ -84,8 +81,6 @@ 不适用 未选择钱包 交易活动将出现在这里 - -- - -- 发送中…… 从二维码获取地址 燃料价格越高,您的交易费用就越昂贵,但以太坊网络对交易的处理速度则越快。 @@ -97,7 +92,6 @@ 无法导出钱包 无法加载通证列表 无法创建钱包 - AlphaWallet 全部 币类 高级设置 @@ -115,9 +109,7 @@ 兑换 门票已兑换 使用Token - %1$s%2$s - %1$s (%2$s) 创建卖单 选择要出售的%1$s: 选择要兑换的门票: @@ -181,11 +173,9 @@ DD/MM/YY - 以太币 门票数量 总价: 相当于USD - %1$s %2$s 选择%1$s数量 你的门票已经被兑换 搜索: @@ -198,7 +188,6 @@ 助记词是做什么的? 怎样把以太币转到AlphaWallet内? 什么是 TokenScript? - tokenscript_explaination.html 24小时 升值 %1$s/门票 @@ -241,7 +230,6 @@ 创建链接 创建销售链接 放上市场待售 - --:-- 链接过期时间 请输入有效时间 请输入有效日期 @@ -265,11 +253,6 @@ 重要 0.00 以太币 更换当前钱包 - Facebook - Twitter - ERC 20 - ERC 875 - ERC 875* 以太坊是一个建立在区块链技术之上的开发平台,同时具有智能合约功能的操作系统。它是区块链产品和服务的基础技术。\n\nETH/Ether 是一个加密货币,其区块链由以太坊平台生成。 Ether 可以在在账户中互相转移和用来奖励给挖矿者。以太坊提供了一个“图灵完备”的虚拟机,称为以太坊虚拟机(Ethereum Virtual Machine),简称EVM,用于国际网络中执行交易代码, \"Gas\", 是一种特别的单位用于Ethereum(以太币)里,它用来衡量一个行为或者一系列行为有多少“工作量”。 目前,以太坊是最好区块链技术和开发智能合约应用程序和。AlphaWallet 开发于太坊区块链技术基础上,目前还在不断创新。 您可以将以太币转换为法定货币,并通过任何加密货币市场转移到您的银行账户。 @@ -384,14 +367,12 @@ 领取加密货币 货币导入 您将收到 - 扫描二维码 无效扫描结果 以太坊地址 刚刚扫描了一个以太坊地址。您是否要尝试将此作为通证进行加载? 加载通证 %1$s区块链 - 0.00%s 电报 (客户支持) 转账请求 通证转账请求: %1$s @@ -426,6 +407,7 @@ 搜索或键入网址 网络 设置启用的区块链 + 启用网络 (%s) 收藏品 通证功能 Alphawallet 通知渠道 @@ -534,7 +516,6 @@ 在 HTML 元素中找不到函数输入\'%1$s\'。 无效输入值:%1$s - LinkedIn 无法创建密钥。请尝试在手机上启用安全功能。 助记词、恢复助记词或备份助记词均属于词语列表,可存储用于恢复以太坊钱包所需的所有信息。钱包软件通常会生成助记词,并指示用户将其记在纸上。 交易太大 @@ -543,10 +524,8 @@ 签名详细信息 复制合约地址 自己 - ERC721T 无法签署兑换消息 通证类型 - Reddit TokenScript 标准兼容性 版本 TokenScript 调试 @@ -584,10 +563,8 @@ 控制台 清除浏览器缓存 重新加载通证数据 - TokenScript 更换语言 添加/隐藏通证 - Instagram 更改货币 选择法定货币 TokenScript 文件错误 @@ -619,7 +596,6 @@ 调用 TokenScript 交易时参数无效。请检查输入。 TokenScript 被%1$s%2$s覆盖\n\n 原始通证:\n\n - %1$s -> %2$s\n\n (调试) 确认文件删除操作 删除文件 %1$s @@ -655,7 +631,6 @@ 隐藏的通证 忽略 搜索通证... - WalletConnect 结束会话 发送以太币交易 遭到用户拒绝 @@ -664,7 +639,6 @@ 交易被拒绝(尝试使用更多的燃料) 来源地址 %1$s 来源地址 - %1$s %2$s 接收地址 %1$s 批准转让代币了 %1$s 代表地址 %1$s @@ -680,10 +654,8 @@ 先前的WalletConnect会话未正确终止。请返回网页并断开连接,然后开始新的会话。 无效的WalletConnect二维码数据 Connection attempt timed out. Please cancel the request from the Dapp and start a new session. - Nonce 数据 签署的交易 - %1$s 删除此会话记录? Session has been terminated. Switch back to Dapp and start a new WalletConnect session. Transaction not sent @@ -779,8 +751,6 @@ %1$s Assets | %2$s This is Testnet Swap with Quickswap - Email - Discord Name This Wallet Enter Wallet Name Save Name @@ -835,7 +805,6 @@ Performance Search token 资产 - DeFi 治理 NFT 什么是新的? @@ -849,10 +818,9 @@ Unkown Network This dapp is requesting to change to an unknown chain: %1$s - No Active sessions + 没有活跃会话 Connect Wallet 助记符只能包含单词 - Token # External Link Description Details @@ -865,7 +833,6 @@ I already have a Wallet Select Mode Network Info - Testnet Website Light Dark @@ -876,6 +843,12 @@ Use 1559 Transactions Experimental 1559 Transactions 重置 + 方法 + 与基于浏览器的 Dapp 的活跃连接 + 用户断开会话. + 名称 + 钱包 + 钱包不存在或者你只能查看钱包 Chain ID: %1d Created By Token Standard @@ -889,7 +862,6 @@ Unable to store key: %1$s ENS Lookup Warning AlphaWallet has detected a discrepancy between ethereum\'s timestamp and your device\'s current time, or possibly the blockchain connection is not synchronized, please check your phone\'s time is correct, or ignore this warning. - 0 减少 增加 The transaction has timed out. You can view the status in activity page. @@ -914,19 +886,71 @@ under 1 second over 1 second not responding + 暂不支持链 %s。 + 连接前必须启用网络 %s。 + Select Token + Custom + WalletConnect 处于活动状态 + 点击查看活跃会话 测试网模式 我的代币在哪里? 不用担心,您的代币是安全的。您正在查看 Testnet 网络,开发人员使用它们来尝试新设计。您可以随时切换到主网。 切换到主网 - Select Token - Custom Name Note - URL Connect Connect to Sign This will inform the remote site your wallet address is %s Floor Price Average Price + Provider + Delete Empty + Delete All + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + 余额不足 %1$s + 安全转账 + 隐私政策 + 服务条款 + 已连接的钱包 + 稀有度 + 首选交易所 + 汽油费: %s + 获取路线 + 选择路线 + 选择交易所 + 新航线 %s + 交换金额 + 提供者网站 + 报价详情 + 通过 %s 交换 + 没有找到给定参数的路由。 + 至少选择一个交易所。 + 已弃用 + 无法对这个仅限手表的钱包执行操作。 + 掃描中:請等待完成 + 為選定的代幣啟用鏈? + (无题) + 会议详情 + 会议提案 + 钱包不同于活动钱包。 + 分析 + 崩溃报告 + 与我们分享您的匿名数据,帮助我们改进 AlphaWallet。 这不包括任何财务信息。 + 共享匿名数据? + 启用所需的链? + 选定的代币位于当前未选择的链上,启用未选择的链? diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 13c29970b6..379baa0d98 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -36,8 +36,12 @@ + + + + @@ -72,6 +76,7 @@ + @@ -106,6 +111,9 @@ + + + @@ -119,8 +127,6 @@ - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1626b4e3a3..fbe541203a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -66,6 +66,7 @@ @color/dove @color/white + @color/solitude @color/translucent_light @color/transparent diff --git a/app/src/main/res/values/colors_misc.xml b/app/src/main/res/values/colors_misc.xml index e76582bb6b..4cdd9ccc42 100644 --- a/app/src/main/res/values/colors_misc.xml +++ b/app/src/main/res/values/colors_misc.xml @@ -14,10 +14,6 @@ #2986AF - #FF4A8D - #70578D - #F6C343 - #6B35A2 #48A9A6 #378937 #5838A3 @@ -49,5 +45,5 @@ #B2C6D8 #222222 #292929 - #8176c2 + #67b1d4 diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 0000000000..3b89ec335c --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,7 @@ + + + + 1.3 + 3.2 + 0.6 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b98fe13e60..829539dc2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,7 +22,7 @@ No, repeat Transfer %s Buy %s - + Try again Already have a wallet? Create a New Wallet @@ -76,8 +76,8 @@ More details Add wallet View in Block Explorer - Coinbase - LocalEthereum + Coinbase + LocalEthereum Changelly (Buy with Credit card) Transaction detail @@ -92,8 +92,8 @@ N/A Wallet is not selected Activities will appear here - -- - -- + -- + -- Sending… Get address from QR Code The higher the gas price, the more expensive your transaction fee will be, but the quicker your transaction will be processed by the Ethereum network. @@ -106,7 +106,7 @@ Failed to load tokens list Failed to create a wallet - AlphaWallet + AlphaWallet All Currencies Advanced Options @@ -124,9 +124,9 @@ Redeem Tickets Redeemed Use Token - %1$s%2$s + %1$s%2$s - %1$s (%2$s) + %1$s (%2$s) Create Sales Order Select %1$s to Sell: Select Tickets to Redeem:show @@ -191,11 +191,10 @@ DD/MM/YY - ETH Quantity of Tickets Total Cost: Equivalent in USD - %1$s %2$s + %1$s %2$s Select Quantity of %1$s Your ticket has been redeemed Search for: @@ -208,11 +207,11 @@ What is a Seed Phrase? How do I transfer ETH into my wallet? What is TokenScript? - Privacy Policy - Terms of Service + Privacy Policy + Terms of Service privacyPolicy.html termsOfService.html - tokenscript_explaination.html + tokenscript_explaination.html 24 hours Appreciation %1$s/Ticket @@ -255,7 +254,7 @@ Share MagicLink Generate MagicLink List on marketplace - --:-- + --:-- MagicLink Expiry Time Please select expiry time Please select expiry date @@ -279,11 +278,11 @@ Important 0.00 ETH Change Wallet - Facebook - Twitter - ERC 20 - ERC 875 - ERC 875* + Facebook + Twitter + ERC 20 + ERC 875 + ERC 875* ERC 1155 Ethereum is an open-source, public, blockchain-based distributed computing platform and operating system featuring smart contract (scripting) functionality. It is the infrastructure for blockchain products and services.\n\nETH/Ether is a cryptocurrency whose blockchain is generated by the Ethereum platform. Ether can be transferred between accounts and used to compensate participant mining nodes for computations performed. Ethereum provides a decentralised Turing-complete virtual machine, the Ethereum Virtual Machine (EVM), which can execute scripts using an international network of public nodes. "Gas", an internal transaction pricing mechanism, is used to mitigate spam and allocate resources on the network. Currently, Ethereum is the best and largest blockchain for developing smart contract application and services. AlphaWallet starts with supporting Ethereum blockchain and helps improving its technology. @@ -402,14 +401,14 @@ Take Currency Drop Currency Import you will receive - + Scan QR Code Invalid Scan Result Ethereum Address Just scanned an Ethereum address. Do you want to try to load this as a Token? Load Token %1$s Blockchain - 0.00 %s + 0.00 %s Telegram (Customer Support) Transfer request Token transfer request: %1$s @@ -444,6 +443,7 @@ Search or type web address Networks Select Active Networks + Enabled Networks (%s) Collectibles Token Function Alphawallet notification channel @@ -552,7 +552,7 @@ function input \'%1$s\' not found in HTML elements. Invalid input value: %1$s Value - LinkedIn + LinkedIn Unable to create key. Try enabling security on your phone. A seed phrase, seed recovery phrase or backup seed phrase is a list of words which store all the information needed to recover a crypto wallet. Wallet software will typically generate a seed phrase and instruct the user to write it down on paper. Transaction Too Large @@ -561,11 +561,11 @@ Signature details Copy Contract Address Self - ERC721T + ERC721T Failed to sign redeem message Token Type - Reddit - TokenScript Compatibiity + Reddit + TokenScript Compatibility Version TokenScript debug untrusted @@ -603,16 +603,16 @@ Console Clear Browser Cache Reload Token Data - TokenScript + TokenScript Change Language Add / Hide Tokens - Instagram + Instagram Change Currency Select FIAT Currency TokenScript file error Browser cache cleared Token Data cleared - Connected Wallets + Connected Wallets Token data being cleared … Please wait Your Wallets Copy @@ -640,7 +640,7 @@ Invalid parameter while calling TokenScript Transaction. Please check inputs. TokenScript overridden by %1$s%2$s\n\n Origin Tokens:\n\n - %1$s -> %2$s\n\n + %1$s -> %2$s\n\n (Debug) Confirm File Delete Delete file %1$s @@ -676,7 +676,7 @@ Hidden Tokens Ignore Search for Tokens... - WalletConnect + WalletConnect End Session Send ETH Transaction Rejected by the user @@ -685,7 +685,7 @@ Transaction rejected (try using more gas) from: %1$s from - %1$s %2$s + %1$s %2$s to: %1$s %1$s: %2$s approved to transfer tokens: %1$s @@ -696,17 +696,17 @@ Approved Approval granted TokenScript call: - Testnet + Testnet Today Go to Token Invalid WalletConnect Session Previous WalletConnect session not correctly terminated. Please return to webpage and disconnect, then start a new session. Invalid WalletConnect QR data Connection attempt timed out. Please cancel the request from the Dapp and start a new session. - Nonce + Nonce Payload Signed Transactions - %1$s + %1$s Delete this session record? Session has been terminated. Switch back to Dapp and start a new WalletConnect session. Transaction not sent @@ -824,13 +824,13 @@ (max. %1$s) This is Testnet Swap with Quickswap - Email - Discord + Email + Discord Name This Wallet Enter Wallet Name Save Name Select Amount - Safe Transfer + Safe Transfer Safe Batch Transfer #%1$s Selected Tokens @@ -875,7 +875,7 @@ Performance Search token Assets - DeFi + DeFi Governance Collectibles What\'s new? @@ -889,10 +889,10 @@ Unknown Network This dapp is requesting to change to an unknown chain: %1$s - No Active sessions + No active sessions Connect Wallet Seed phrase can only contain words - Token # + Token # External Link Description Details @@ -913,6 +913,13 @@ %1$s%% have this trait Use 1559 Transactions Experimental 1559 Transactions + Methods + WalletConnect + Active connection to browser-based Dapp + User disconnect the session. + Name + Wallet + Wallet not exist or it\'s watch only Chain ID: %1d Created By Token Standard @@ -926,7 +933,6 @@ Unable to store key: %1$s ENS Lookup Warning AlphaWallet has detected a discrepancy between ethereum\'s timestamp and your device\'s current time, or possibly the blockchain connection is not synchronized, please check your phone\'s time is correct, or ignore this warning. - 0 Decrease Increase The transaction has timed out. You can view the status in activity page. @@ -934,7 +940,8 @@ Slippage Open Settings No connections found for this chain. - Fees + Gas Fee + Other Fees Current Price Minimum Received Balance: @@ -953,6 +960,10 @@ under 1 second over 1 second not responding + Chain %s is not supported. + Network %s must be enabled before connecting. + WalletConnect is active + Click to view active sessions Testnet Mode Where are my Tokens? Don\'t worry. Your tokens are safe. You are viewing Testnet networks. They are used by developers to try out new designs. You can switch to Mainnet any time. @@ -963,11 +974,58 @@ 0.01 Name Note - URL + URL Connect Connect to Sign This will inform the remote site your wallet address is %s Floor Price Average Price + Provider + Delete Empty + Delete All + Buy with Coinbase Pay + Buy with Ramp + Buy with Coinbase Pay + Key Status + Fix Key State + Run Key Diagnostic + Key Diagnostic + Key found + Unable to find Key + Seed Phrase detected public key: %1$s + Decoded Keystore public key: %1$s + Locked + Key type in Database + Key Entry in Secure Enclave + | + ETH + 0 + Rarity + Preferred Exchanges + Gas Fee: %s + Fetching Routes + Select Route + Select Exchanges + New Routes in %s + Amount To Swap + Provider Website + Quote Details + Swap via %s + No routes found for the given parameters. + Select at least one exchange. + Deprecated + Could not perform action on this Watch-only wallet. + Scanning in progress: Please wait for completion + Enable chains for selected tokens? + (No title) + Session Details + Session Proposal + Wallet different from active wallet. + Analytics + Crash Reporting + Help us to improve AlphaWallet by sharing your anonymous data with us. This does not include any financial information. + Share Anonymous Data? + Enable required chains? + Selected tokens are on chains currently not selected, enable unselected chains? diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9defc3328a..c456b04436 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -198,4 +198,7 @@ @style/Aw.Typography + diff --git a/app/src/main/res/values/typography.xml b/app/src/main/res/values/typography.xml index a4dbceec6b..fc9127f41e 100644 --- a/app/src/main/res/values/typography.xml +++ b/app/src/main/res/values/typography.xml @@ -70,6 +70,12 @@ ?android:textColorSecondary + +