Compare commits

...

No commits in common. "wallet" and "archive/project-docs" have entirely different histories.

10962 changed files with 8529 additions and 453786 deletions

View file

@ -1,929 +0,0 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 160
tab_width = 4
ij_continuation_indent_size = 4
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false
# Ktlint rule, for more information see https://pinterest.github.io/ktlint/latest/faq/#how-do-i-enable-or-disable-a-rule
ktlint_standard_wrapping = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_spacing-between-declarations-with-annotations = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_annotation = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_indent = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_function_naming_ignore_when_annotated_with = Composable
# Added when upgrading to 1.7.1
ktlint_standard_function-expression-body = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
# Added when upgrading to 1.8.0
ktlint_standard_when-entry-bracing = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_mixed-condition-operators = disabled
ktlint_standard_no-unused-imports = enabled
[*.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 = end_of_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 = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_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 = never
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 = false
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 = false
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 = false
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 = end_of_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 = false
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 = true
ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_assignment_wrap = off
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 = false
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = off
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = off
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = true
ij_kotlin_continuation_indent_in_if_conditions = true
ij_kotlin_continuation_indent_in_parameter_lists = true
ij_kotlin_continuation_indent_in_supertype_lists = true
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = off
ij_kotlin_field_annotation_wrap = normal
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
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 = 0
ij_kotlin_keep_blank_lines_in_code = 1
ij_kotlin_keep_blank_lines_in_declarations = 1
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 = off
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 = off
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_packages_to_use_import_on_demand = kotlinx.android.synthetic.**
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 = 0
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
[{*.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
[**/generated/**]
generated_code = true
ij_formatter_enabled = false
ktlint = disabled

View file

@ -1,40 +0,0 @@
# .forgejo/workflows/gitleaks.yml
#
# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
# (task #295).
#
# Pairs with the pre-receive hook installed on every bare repo — that one is
# the strict enforcement layer (rejects the push); this one provides the
# per-PR red ✗ that branch-protection rules can require before merge.
#
# Layer 1 (this workflow): visible per-PR status, can be a required check.
# Layer 2 (pre-receive hook): strict enforcement at the server.
# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos.
name: gitleaks
on:
push:
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history — gitleaks needs depth to scan a commit range.
fetch-depth: 0
- name: install gitleaks
run: |
curl -sSL -o gl.tar.gz \
https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
tar xzf gl.tar.gz gitleaks
chmod +x gitleaks
./gitleaks version
- name: scan
run: |
./gitleaks detect --source . --no-banner --redact --verbose

5
.gitattributes vendored
View file

@ -1,5 +0,0 @@
screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
libraries/compound/screenshots/** filter=lfs diff=lfs merge=lfs -text
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
**/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text
libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text

View file

@ -1,116 +0,0 @@
name: Upstream sync
# Daily check against the upstream mirror. Fast-forwards `main` to
# `upstream/develop` when upstream has advanced, then pings the Infra
# Matrix room so we know the wallet branch is due for a rebase.
#
# See SYNC.md on the wallet branch for the full topology + procedure
# this job implements.
on:
schedule:
# 12:00 UTC daily — quiet time for all our time zones, avoids the
# morning-meeting window where an unexpected Matrix ping is noise.
- cron: '0 12 * * *'
workflow_dispatch: # manual trigger from the Actions UI too
jobs:
sync-main:
runs-on: ubuntu-latest
env:
# The repo's .gitattributes (inherited from upstream) routes the
# screenshots/ tree through git-lfs. Gitea's LFS store doesn't hold
# those blobs, so on checkout the smudge filter tries to 404-download
# them and wedges git state for subsequent fetches. We don't need
# the image bytes here — leave LFS pointers as-is.
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
lfs: false
# Gitea's built-in GITEA_TOKEN is read-only by default.
# GIT_PUSH_TOKEN is a repo secret with a write-scoped PAT, so
# the subsequent `git push origin main` actually lands.
token: ${{ secrets.GIT_PUSH_TOKEN }}
- name: Fetch upstream + wallet
run: |
set -euo pipefail
# Fetch directly from GitHub. We also have a Gitea pull-mirror
# at Sulkta-Coop/element-x-upstream that tracks this same repo,
# but sourcing from GitHub keeps the workflow independent of
# the mirror's health — one less moving part to diagnose.
git remote add upstream https://github.com/element-hq/element-x-android.git
git fetch --depth=500 upstream develop
git fetch origin wallet:refs/remotes/origin/wallet
- name: Fast-forward main
id: ff
run: |
set -euo pipefail
git config user.name "sulkta-bot"
git config user.email "bot@sulkta.com"
# git-lfs pre-push hook refuses incomplete pushes — which triggers
# here because we skipped LFS smudge on checkout, so local LFS
# objects are absent. We're only pushing branch pointers (no new
# LFS content), so allow incomplete.
git config lfs.allowincompletepush true
OLD=$(git rev-parse --short HEAD)
echo "main was at $OLD"
if git merge --ff-only upstream/develop; then
NEW=$(git rev-parse --short HEAD)
if [ "$OLD" = "$NEW" ]; then
echo "main already up to date with upstream/develop"
echo "advanced=false" >> "$GITHUB_OUTPUT"
else
echo "main advanced: $OLD -> $NEW"
git push origin main
echo "advanced=true" >> "$GITHUB_OUTPUT"
echo "old=$OLD" >> "$GITHUB_OUTPUT"
echo "new=$NEW" >> "$GITHUB_OUTPUT"
fi
else
echo "::warning::main could not fast-forward to upstream/develop — someone committed to main directly?"
echo "advanced=false" >> "$GITHUB_OUTPUT"
fi
- name: Measure wallet drift
if: steps.ff.outputs.advanced == 'true'
id: drift
run: |
set -euo pipefail
MB=$(git merge-base refs/remotes/origin/wallet main)
BEHIND=$(git rev-list --count "$MB..main")
NEW_ADDED=$(git rev-list --count "$MB..upstream/develop")
echo "behind=$BEHIND" >> "$GITHUB_OUTPUT"
echo "new_added=$NEW_ADDED" >> "$GITHUB_OUTPUT"
echo "wallet is $BEHIND commits behind main now; $NEW_ADDED new upstream commits this run"
- name: Matrix notification (Infra room)
# Best-effort — if the target bot isn't in the room or Matrix is
# flapping, don't fail the whole run. The advance + push is the
# critical path; notify is a convenience ping.
if: steps.ff.outputs.advanced == 'true'
continue-on-error: true
env:
MATRIX_TOKEN: ${{ secrets.MATRIX_HOUSE_BOT_TOKEN }}
run: |
set -euo pipefail
TXN=$(date +%s%N)
ROOM='!rvxiUrWpgvMTAwzjGm:sulkta.com' # Infra
BODY="element-x upstream advanced · main ${{ steps.ff.outputs.old }} → ${{ steps.ff.outputs.new }} (${{ steps.drift.outputs.new_added }} commits). wallet is ${{ steps.drift.outputs.behind }} commits behind — rebase before next build."
# jq keeps the body properly JSON-escaped; safer than shell interp
# shellcheck disable=SC2086
PAYLOAD=$(printf '%s' "$BODY" | jq -Rs '{msgtype: "m.text", body: .}')
curl --fail -s -X PUT \
-H "Authorization: Bearer $MATRIX_TOKEN" \
-H "Content-Type: application/json" \
"https://chat.sulkta.com/_matrix/client/v3/rooms/${ROOM}/send/m.room.message/${TXN}" \
-d "$PAYLOAD"
echo "notified"

View file

@ -1,86 +0,0 @@
name: Bug report for the Element X Android app
description: Report any issues that you have found with the Element X app. Please [check open issues](https://github.com/element-hq/element-x-android/issues) first, in case it has already been reported.
labels: [T-Defect]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please report security issues by email to security@matrix.org
- type: textarea
id: reproduction-steps
attributes:
label: Steps to reproduce
description: Please attach screenshots, videos or logs if you can.
placeholder: Tell us what you see!
value: |
1. Where are you starting? What can you see?
2. What do you click?
3. More steps…
validations:
required: true
- type: textarea
id: result
attributes:
label: Outcome
placeholder: Tell us what went wrong
value: |
#### What did you expect?
#### What happened instead?
validations:
required: true
- type: input
id: device
attributes:
label: Your phone model
placeholder: e.g. Samsung S6
validations:
required: false
- type: input
id: os
attributes:
label: Operating system version
placeholder: e.g. Android 10.0
validations:
required: false
- type: input
id: version
attributes:
label: Application version and app store
description: You can find the version information at the bottom of the Settings screen.
placeholder: e.g. Element X version 0.3.0
validations:
required: false
- type: input
id: homeserver
attributes:
label: Homeserver
description: |
Which server is your account registered on? If it is a local or non-public homeserver, please tell us what is the homeserver implementation (ex: Synapse/Dendrite/etc.) and the version.
placeholder: e.g. matrix.org or Synapse 1.50.0rc1
validations:
required: false
- type: dropdown
id: rageshake
attributes:
label: Will you send logs?
description: |
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
options:
- 'Yes'
- 'No'
validations:
required: true
- type: dropdown
id: pr
attributes:
label: Are you willing to provide a PR?
description: |
Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
options:
- 'Yes'
- 'No'
validations:
required: true

View file

@ -1,47 +0,0 @@
name: Enhancement request
description: Do you have a suggestion or feature request?
labels: [T-Enhancement]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas).
- type: textarea
id: usecase
attributes:
label: Your use case
description: Please feel welcome to include screenshots or mock ups.
placeholder: Tell us what you would like to do!
value: |
#### What would you like to do?
#### Why would you like to do it?
#### How would you like to achieve it?
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Have you considered any alternatives?
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
placeholder: Is there anything else you'd like to add?
validations:
required: false
- type: dropdown
id: pr
attributes:
label: Are you willing to provide a PR?
description: |
Don't worry, it's still OK to answer 'No' :).
options:
- 'Yes'
- 'No'
validations:
required: true

View file

@ -1,11 +0,0 @@
---
name: Task that belongs to a story/epic
about: A skeleton task where the details are all contained within a story or epic
on element-meta.
title: "[Task] "
labels: T-Task
assignees: ''
---
Please see and discuss the details in the meta issue.

View file

@ -1,27 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Updates for Github Actions used in the repo
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
cooldown:
default-days: 7
# Updates for Gradle dependencies used in the app
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
cooldown:
default-days: 7

View file

@ -1,64 +0,0 @@
<!--
Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request.
Are you adding a new feature? Keep in mind that it needs to be added to [the iOS client](https://github.com/element-hq/element-x-ios) too, unless it's related to an Android OS only behaviour.
**IMPORTANT:** if you are adding new screens or modifying existing ones, this needs acceptance from the product and design teams before being merged. For this, it's better to start with a [feature request issue](https://github.com/element-hq/element-x-android/issues/new?template=enhancement.yml) describing the change you want to make and the motivation behind it instead of directly creating a pull request. This will allow the product and design teams to give feedback on the change before you start working on it, and avoid you doing work that might end up being rejected.
-->
## Content
<!-- Describe shortly what has been changed -->
## Motivation and context
<!-- Provide link to the corresponding issue if applicable or explain the context -->
## Screenshots / GIFs
<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.
Providing a video of the change is still very useful for the reviewer and for the history of the project.
You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|
|Before|After|
|-|-|
|||
-->
## Tests
<!-- Explain how you tested your development -->
- Step 1
- Step 2
- Step ...
## Tested devices
- [ ] Physical
- [ ] Emulator
- OS version(s):
## Checklist
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
- This PR was made with the help of AI:
- [ ] Yes. In this case, please request a review by Copilot.
- [ ] No.
- [ ] Changes have been tested on an Android device or Android emulator with API 24
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly defines what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] You've made a self review of your PR

34
.github/release.yml vendored
View file

@ -1,34 +0,0 @@
changelog:
categories:
- title: ✨ Features
labels:
- PR-Feature
- title: 🙌 Improvements
labels:
- PR-Change
- title: 🐛 Bugfixes
labels:
- PR-Bugfix
- title: ⚠️ API Changes
labels:
- PR-Api
- title: 🗣 Translations
labels:
- PR-i18n
- title: 🧱 Build
labels:
- PR-Build
- title: 📄 Documentation
labels:
- PR-Doc
- title: 🚧 In development 🚧
labels:
- PR-Wip
- title: Dependency upgrades
labels:
- PR-Dependencies
- title: Others
labels:
- PR-misc
- "*"

View file

@ -1,47 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
],
"labels": [
"PR-Dependencies",
],
"ignoreDeps": [
"string:app_name",
"gradle",
],
"minimumReleaseAge": "7 days",
"packageRules": [
{
"matchPackageNames": [
"org.matrix.rustcomponents:sdk-android",
"/^io.element.android/",
],
"minimumReleaseAge": null,
},
{
"groupName": "kotlin",
"matchPackageNames": [
"/^org.jetbrains.kotlin/",
"/^com.google.devtools.ksp/",
"/^androidx.compose.compiler/",
],
},
{
"versioning": "semver",
"matchPackageNames": [
"/^org.maplibre/",
"/^org.jetbrains.kotlinx:kotlinx-datetime/",
],
},
{
// Limit PostHog Android upgrade to one PR per month, the first day of the month
"matchPackageNames": [
"com.posthog:posthog-android",
],
"schedule": [
"* * 1 * *",
]
},
],
}

117
.gitignore vendored
View file

@ -1,117 +0,0 @@
# Copyright (c) 2025 Element Creations Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
# Please see LICENSE files in the repository root for full details.
# Built application files
*.apk
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Python cache
__pycache__/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/.name
.idea/androidTestResultsUserPreferences.xml
.idea/assetWizardSettings.xml
.idea/AndroidProjectSystem.xml
.idea/compiler.xml
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/deviceManager.xml
.idea/gradle.xml
.idea/jarRepositories.xml
.idea/markdown.xml
.idea/misc.xml
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
.idea/other.xml
.idea/runConfigurations.xml
.idea/tasks.xml
.idea/workspace.xml
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/copilot
.idea/copilot.*
.idea/inspectionProfiles
# Shelved changes in the IDE
.idea/shelf
.idea/sonarlint
# .kotlin folder
.kotlin
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
/tmp
.DS_Store
checkouts/**

View file

@ -1,38 +0,0 @@
# gitleaks config — element-x-ada
#
# Element X is a Matrix client fork with Cardano ADA integration.
# Patterns flagged are all public-by-design or doc/test fixtures:
# - PostHog apiKey: client-side analytics token, public on every PostHog-
# integrated mobile app. Identifies the project, doesn't grant write.
# - MapTiler API_KEY: client-side maps token, ships in every release
# - google-services.json: Firebase config — Google explicitly documents
# this as public-by-design (all real auth goes through FirebaseAuth)
# - Segment readKey: client-side write key
# - user_signing_key in KDoc comments: example values in doc-strings
# - docs/ + *Test.kt files: scratch + test fixtures, never live credentials
[extend]
useDefault = true
[allowlist]
description = "Public client keys (PostHog, MapTiler, Firebase, Segment) + docs + test fixtures"
paths = [
'''docs/.*''',
'''.*/google-services\.json''',
'''.*Test\.kt''',
'''localazy\.json''',
'''tools/localazy/.*''',
]
regexTarget = "line"
regexes = [
# PostHog client keys — match any variable name ending in apiKey
'''[a-zA-Z]*[Aa]piKey\s*=\s*"phc_[A-Za-z0-9_-]{20,}"''',
# MapTiler / similar public client keys named API_KEY constant
'''const\s+val\s+API_KEY\s*=\s*"''',
# Segment write keys (Kotlin style)
'''readKey\s*=\s*"''',
# Localazy / Segment readKey (JSON style)
'''"readKey"\s*:\s*"''',
# Matrix protocol KDoc examples (* prefix is the KDoc comment shape)
'''^\s*\*\s*"user_signing_key"\s*:\s*"''',
]

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "enterprise"]
path = enterprise
url = git@github.com:element-hq/element-android-enterprise.git

View file

@ -1,124 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View file

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;Element Creations Ltd, Element Software SARL, Element Software Inc.,&#10;and Element Software GmbH (the &quot;Element Group&quot;) only make this file available&#10;under a proprietary license model.&#10;&#10;Without a proprietary license with us, you cannot use this file. The terms of&#10;the proprietary license agreement between you and any member of the Element Group&#10;shall always apply to your use of this file. Unauthorised use, copying, distribution,&#10;or modification of this file, via any medium, is strictly prohibited.&#10;&#10;For details about the licensing terms, you must either visit our website or contact&#10;a member of our sales team." />
<option name="myName" value="Element Enterprise" />
</copyright>
</component>

View file

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (c) &amp;#36;today.year Element Creations Ltd.&#10;&#10;SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.&#10;Please see LICENSE files in the repository root for full details." />
<option name="myName" value="Element FOSS" />
</copyright>
</component>

View file

@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings default="Element FOSS">
<module2copyright>
<element module="Enterprise" copyright="Element Enterprise" />
</module2copyright>
</settings>
</component>

View file

@ -1,27 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="shared">
<words>
<w>agpl</w>
<w>backstack</w>
<w>blurhash</w>
<w>fdroid</w>
<w>ftue</w>
<w>gplay</w>
<w>homeserver</w>
<w>konsist</w>
<w>kover</w>
<w>measurables</w>
<w>onboarding</w>
<w>placeables</w>
<w>posthog</w>
<w>rageshake</w>
<w>securebackup</w>
<w>showkase</w>
<w>snackbar</w>
<w>spdx</w>
<w>swipeable</w>
<w>textfields</w>
<w>tombstoned</w>
</words>
</dictionary>
</component>

BIN
.idea/icon.png generated

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

6
.idea/kotlinc.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.20" />
</component>
</project>

10
.idea/migrations.xml generated
View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View file

@ -1,3 +0,0 @@
<component name="DependencyValidationManager">
<scope name="Enterprise" pattern="file://*||file[ElementX.enterprise*]:*//*" />
</component>

View file

@ -1,74 +0,0 @@
# Maestro
Maestro is a framework that we are using to test navigation across the application.
To setup, please refer at [https://maestro.mobile.dev](https://maestro.mobile.dev)
<!--- TOC -->
* [Run test](#run-test)
* [Output](#output)
* [Write test](#write-test)
* [CI](#ci)
* [iOS](#ios)
* [Future](#future)
<!--- END -->
## Run test
From root dir of the project
*Note: Since Element X does not allow account creation, we have to use an existing account to run maestro test suite. So to run locally, please replace `user` and `123` with your test matrix.org account credentials, and `my room` with one of a room this account has joined. Note that the test will send messages to this room.*
```shell
maestro test \
-e MAESTRO_APP_ID=io.element.android.x.debug \
-e MAESTRO_USERNAME=user1 \
-e MAESTRO_PASSWORD=123 \
-e MAESTRO_RECOVERY_KEY=ABC \
-e MAESTRO_ROOM_NAME="MyRoom" \
-e MAESTRO_INVITEE1_MXID=user2 \
-e MAESTRO_INVITEE2_MXID=user3 \
.maestro/allTests.yaml
```
### Output
Test result will be printed on the console, and screenshots will be generated at `./build/maestro`
## Write test
Tests are yaml files. Generally each yaml file should leave the app in the same screen than at the beginning.
Start the Element X app and run this command to help writing test.
```shell
maestro studio
```
Note that sometimes, this prevent running the test. So kill the `maestro studio` process to be able to run the test again.
Also, if updating the application code, do not forget to deploy again the application before running the maestro tests.
## CI
The CI is running maestro using the workflow `.github/worflow/maestro.yaml` and [maestro cloud](https://cloud.mobile.dev/). For now we are limited to 100 runs a month.
Some GitHub secrets are used to be able to do that: `MAESTRO_CLOUD_API_KEY`, for now api key from `benoitm@element.io` maestro cloud account, and `MATRIX_MAESTRO_ACCOUNT_PASSWORD` which is the password of the account `@maestroelement:matrix.org`. This account contains a room `MyRoom` to be able to run the maestro test suite.
## iOS
Need to install `idb-companion` first
```shell
brew install idb-companion
```
Also:
https://github.com/mobile-dev-inc/maestro/issues/146
https://github.com/mobile-dev-inc/maestro/issues/107
So you have to change your input keyboard to QWERTY for it to work properly.
## Future
- run on Element X iOS. This is already working but it need some change on the test to make it works. Could pass a PLATFORM parameter to have unique test and use conditional test.
- run specific test on both iOS and Android devices to make them communicate together. Could be possible to test room invite and join, verification, call, etc. To be done when Element X will be able to create account and create room. A main script would be able to detect the Android device and the iOS device, and run several maestro tests sequentially, using `--device` parameter to perform a global test.

View file

@ -1,10 +0,0 @@
appId: ${MAESTRO_APP_ID}
androidWebViewHierarchy: devtools
---
## Check that all env variables required in the whole test suite are declared (to fail faster)
- runScript: ./scripts/checkEnv.js
- runFlow: tests/init.yaml
- runFlow: tests/account/login.yaml
- runFlow: tests/settings/settings.yaml
- runFlow: tests/roomList/roomList.yaml
- runFlow: tests/account/logout.yaml

View file

@ -1,10 +0,0 @@
// This array contains all the required environment variable. When adding a variable, add it here also.
// If a variable is missing, an error will occur.
if (MAESTRO_APP_ID == null) throw "Fatal: missing env variable MAESTRO_APP_ID"
if (MAESTRO_USERNAME == null) throw "Fatal: missing env variable MAESTRO_USERNAME"
if (MAESTRO_PASSWORD == null) throw "Fatal: missing env variable MAESTRO_PASSWORD"
if (MAESTRO_RECOVERY_KEY == null) throw "Fatal: missing env variable MAESTRO_RECOVERY_KEY"
if (MAESTRO_ROOM_NAME == null) throw "Fatal: missing env variable MAESTRO_ROOM_NAME"
if (MAESTRO_INVITEE1_MXID == null) throw "Fatal: missing env variable MAESTRO_INVITEE1_MXID"
if (MAESTRO_INVITEE2_MXID == null) throw "Fatal: missing env variable MAESTRO_INVITEE2_MXID"

View file

@ -1,21 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "login-change_server"
- takeScreenshot: build/maestro/200-ChangeServer
- tapOn: "matrix.org"
- tapOn:
id: "login-change_server"
- tapOn: "Other"
- tapOn:
id: "change_server-server"
- inputText: "element"
- hideKeyboard
- extendedWaitUntil:
visible: "element.io"
timeout: 10000
- tapOn: "element.io"
# Revert to matrix.org
- tapOn:
id: "login-change_server"
- tapOn: "matrix.org"

View file

@ -1,32 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn
- runFlow: changeServer.yaml
- runFlow: ../assertions/assertLoginDisplayed.yaml
- tapOn:
id: "login-continue"
## MAS page
- retry:
maxRetries: 3
commands:
- extendedWaitUntil:
visible:
id: "form-1"
timeout: 10000
- tapOn:
id: "form-1"
- inputText: ${MAESTRO_USERNAME}
- pressKey: Enter
- tapOn:
id: "form-3"
- inputText: ${MAESTRO_PASSWORD}
- pressKey: Enter
- tapOn: "Continue"
## Back to native world
- runFlow: ../assertions/assertSessionVerificationDisplayed.yaml
- runFlow: ./verifySession.yaml
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
- tapOn: "Not now"
- runFlow: ../assertions/assertHomeDisplayed.yaml

View file

@ -1,15 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "home_screen-settings"
- tapOn: "Remove this device"
- takeScreenshot: build/maestro/900-SignOutScreen
- back
- tapOn: "Remove this device"
# Ensure cancel cancels
- tapOn:
id: "dialog-negative"
- tapOn: "Remove this device"
- tapOn:
id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml

View file

@ -1,19 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Use recovery key"
timeout: 30000
- takeScreenshot: build/maestro/150-Verify
- tapOn: "Use recovery key"
- tapOn:
id: "verification-recovery_key"
- inputText: ${MAESTRO_RECOVERY_KEY}
- hideKeyboard
- tapOn: "Continue"
- retry:
maxRetries: 3
commands:
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- tapOn: "Continue"

View file

@ -1,5 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Help improve Element X dbg"
timeout: 10000

View file

@ -1,5 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Chats"
timeout: 10000

View file

@ -1,5 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Be in your element"
timeout: 30000

View file

@ -1,5 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Change account provider"
timeout: 10000

View file

@ -1,5 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: ${MAESTRO_ROOM_NAME}
timeout: 10000

View file

@ -1,5 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Confirm your digital identity"
timeout: 60000

View file

@ -1,7 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- clearState
- launchApp:
clearKeychain: true
- runFlow: ./assertions/assertInitDisplayed.yaml
- takeScreenshot: build/maestro/000-FirstScreen

View file

@ -1,20 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
# Purpose: Test the creation and deletion of a DM room.
- tapOn: "Create room"
- tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- tapOn: "Send invite"
- takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2"
- scroll
- tapOn: "Leave room"
- tapOn: "Leave"
- runFlow:
when:
visible: 'You need an invite in order to join'
commands:
- tapOn: "Back"

View file

@ -1,44 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
# Purpose: Test the creation and deletion of a room
- tapOn: "Create room"
- tapOn: "New room"
- tapOn: "Add name…"
- inputText: "aRoomName"
- tapOn: "Add description…"
- inputText: "aRoomTopic"
- tapOn: "Create"
- takeScreenshot: build/maestro/320-createAndDeleteRoom
- tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- tapOn: "Finish"
- tapOn: "aRoomName"
- tapOn: "Invite"
# assert there's 1 member and 1 invitee
- tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE2_MXID}
- tapOn:
text: ${MAESTRO_INVITEE2_MXID}
index: 1
- tapOn: "Invite"
- tapOn: "Back"
- tapOn: "aRoomName"
- scrollUntilVisible:
direction: DOWN
element:
text: "People"
- tapOn: "People"
# assert there's 1 member and 2 invitees
- tapOn: "Back"
- scroll
- scroll
- tapOn: "Leave room"
- tapOn: "Leave"
- runFlow:
when:
visible: 'You need an invite in order to join'
commands:
- tapOn: "Back"

View file

@ -1,14 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
# Purpose: Test the context menu of a room in the room list
- longPressOn: ${MAESTRO_ROOM_NAME}
- takeScreenshot: build/maestro/310-RoomList-ContextMenu
- tapOn:
text: "Settings"
index: 0
- tapOn: "Back"
- longPressOn: ${MAESTRO_ROOM_NAME}
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Cancel"

View file

@ -1,8 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- runFlow: searchRoomList.yaml
- takeScreenshot: build/maestro/300-RoomList
- runFlow: timeline/timeline.yaml
- runFlow: roomContextMenu.yaml
- runFlow: createAndDeleteRoom.yaml
- runFlow: createAndDeleteDM.yaml

View file

@ -1,18 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- runFlow: ../assertions/assertRoomListSynced.yaml
- tapOn: "search"
- inputText: ${MAESTRO_ROOM_NAME.substring(0, 3)}
- takeScreenshot: build/maestro/400-SearchRoom
- tapOn: ${MAESTRO_ROOM_NAME}
# Back from timeline to search
- back
- extendedWaitUntil:
visible: ${MAESTRO_ROOM_NAME.substring(0, 3)}
timeout: 10000
# Back to close the keyboard
- back
- waitForAnimationToEnd
# Back to close the home screen
- back
- runFlow: ../assertions/assertHomeDisplayed.yaml

View file

@ -1,13 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn: "Start a call"
- takeScreenshot: build/maestro/700-Call
- extendedWaitUntil:
visible: "maestroelement"
timeout: 10000
- takeScreenshot: build/maestro/710-Call
# Hangup
- tapOn: "End call"
- extendedWaitUntil:
visible: "MyRoom"
timeout: 10000

View file

@ -1,7 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/520-Timeline
- tapOn: "Add attachment"
- tapOn: "Share location"
- tapOn: "Share selected location"
- takeScreenshot: build/maestro/521-Timeline

View file

@ -1,13 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/530-Timeline
- tapOn: "Add attachment"
- tapOn: "Poll"
- tapOn: "What is the poll about?"
- inputText: "I am a poll"
- tapOn: "Option 1"
- inputText: "Answer 1"
- tapOn: "Option 2"
- inputText: "Answer 2"
- tapOn: "Create"
- takeScreenshot: build/maestro/531-Timeline

View file

@ -1,9 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/510-Timeline
- tapOn:
id: "text_editor"
- inputText: "Hello world!"
- tapOn: "Send message"
- hideKeyboard
- takeScreenshot: build/maestro/511-Timeline

View file

@ -1,14 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
# This is the name of one room
- tapOn: ${MAESTRO_ROOM_NAME}
- takeScreenshot: build/maestro/500-Timeline
- runFlow: messages/text.yaml
- runFlow: messages/location.yaml
- runFlow: messages/poll.yaml
# Restore once the call flow is fixed
#- runFlow: call/call.yaml
- back
- runFlow: ../../assertions/assertHomeDisplayed.yaml

View file

@ -1,46 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "home_screen-settings"
- assertVisible: "Settings"
- takeScreenshot: build/maestro/600-Settings
- tapOn:
text: "Analytics"
- assertVisible: "Share analytics data"
- back
- tapOn:
text: "Notifications"
- assertVisible: "Enable notifications on this device"
- back
- tapOn:
text: "Report a problem"
- assertVisible: "Report a problem"
- back
- tapOn:
text: "About"
- assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back
- tapOn:
text: "Screen lock"
- assertVisible: "Choose PIN"
- hideKeyboard
- back
- tapOn:
text: "Advanced settings"
- assertVisible: "View source"
- back
- tapOn:
text: "Developer options"
- assertVisible: "Feature flags"
- back
- back
- runFlow: ../assertions/assertHomeDisplayed.yaml

118
AGENTS.md
View file

@ -1,118 +0,0 @@
# AGENTS.md — Element X Android
> **Repo:** `element-hq/element-x-android` — Android Matrix client (Compose UI + `matrix-rust-sdk`).
---
## Strong Conventions
PRs must meet these rules.
### Code Style
- Style enforced by **Editor config** (`.editorconfig`).
- Set "Hard wrap at" to 160 chars in Android Studio.
### PII & Logging
- We use **Timber** for logging. Never use `android.util.Log`.
- **Never log secrets, passwords, keys, or user content** (e.g. message bodies).
- Matrix IDs (User IDs, Room IDs, Event IDs) are safe to log.
### Strings & Localisation
- Default localisation: `en` (en-GB strings), shared with Element X iOS via [Localazy](https://localazy.com/p/element).
- **Never edit `localazy.xml`** — it is auto-generated and overwritten.
- New English strings go in **`temporary.xml`**. The core team imports these to Localazy.
- **Key naming**:
- Cross-screen verbs: `action_` (e.g., `action_copy`).
- Common nouns/other: `common_` (e.g., `common_error`).
- Accessibility: `a11y_`.
- Screen-specific: `screen_<name>_<key>` (e.g., `screen_onboarding_welcome_title`).
- Errors: `error_` prefix.
- Platform-specific: `_ios` or `_android` suffix.
- Placeholders: Use numbered form `%1$s`, `%2$d`.
### Previews
- Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency.
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
- Wrap previews in `ElementPreview { ... }`.
---
## Pull Request Guidelines
- Use sentence-style commit/PR messages (no conventional commits).
- Apply exactly **one** `PR-` label for changelog categorization.
- PR title = changelog entry — make it descriptive; no "Fixes #…" prefixes.
- Include screenshots or screen recordings for any UI changes.
- Keep PRs focused; split changes over 1000 lines.
---
## Project Structure
### Build System
Common Gradle tasks:
- Build: `./gradlew assembleDebug`
- Unit Tests: `./gradlew test`
- Lint: `./gradlew lint`
- Format: `./gradlew ktlintFormat`
- Update Docs TOC: `./gradlew generateDocsToc`
### Gradle Modules
Features follow a 3-module structure:
- `features/foo/api`: Public interfaces and data classes.
- `features/foo/impl`: Internal implementation, Presenter, and View.
- `features/foo/test`: Test fakes and utilities.
---
## Architecture: Appyx + Molecule
We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecule](https://github.com/cashapp/molecule) for Presenters.
### Files Per Screen (`Foo`)
| File | Purpose |
| :--- | :--- |
| `FooNode.kt` | Appyx Node: Handles navigation and wires the Presenter to the View. |
| `FooPresenter.kt` | A `@Composable` function that produces `FooState` from `FooEvent`s. |
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
| `FooState.kt` | Data class representing the immutable UI state. |
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
---
## Dependency Injection (Metro)
- We use [Metro](https://zacsweers.github.io/metro/) for DI.
- Inject via constructor parameters using `@Inject`.
- Use `@AssistedInject` and `@AssistedFactory` for components requiring runtime arguments (like Navigators or IDs).
- Use `@ContributesBinding(AppScope::class)` for singleton-like services.
- Use `@ContributesNode(RoomScope::class)` for Appyx Nodes.
---
## Compound Design System
Always prefer Compound components and tokens from `libraries/compound/` module.
- **Colours**: `ElementTheme.colors.textPrimary`, `ElementTheme.colors.bgCanvasDefault`.
- **Typography**: `ElementTheme.typography.fontBodyMdRegular`.
- **Icons**: Use `CompoundIcons.IconName()` (e.g., `CompoundIcons.UserProfileSolid()`).
---
## The Rust SDK Layer
We wrap the `matrix-rust-sdk` to isolate the UI from the underlying SDK.
- Naming: SDK `Room``JoinedRoom` or `RoomInfo`.
- Type Mapping: Map Rust SDK types to Kotlin data classes in the `api` module to avoid leaking `MatrixRustSDK` into the UI.
- Always follow Kotlin naming conventions (e.g., `userId` instead of `userID`).

View file

@ -1,17 +0,0 @@
A full developer contributors list can be found [here](https://github.com/element-hq/element-x-android/graphs/contributors).
# Core team:
The element.io Android developer team.
# Other contributors
First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
Feel free to add your name below, when you contribute to the project!
Name | Matrix ID | GitHub
----------|-----------------------------|--------------------------------------
name | @name:matrix.org | [githubID](https://github.com/githubID)

View file

@ -1,221 +0,0 @@
# BLOCKERS.md - Phase 1 Implementation Status
## Task 1: Module Scaffolding ✅ COMPLETE
### Completed
- ✅ Module structure created (api/impl/test)
- ✅ Metro DI setup following Element X patterns
- ✅ WalletEntryPoint and WalletState APIs defined
- ✅ PaymentFlowNode placeholder with Appyx navigation
- ✅ FakeWalletEntryPoint for testing
- ✅ Cardano client library dependencies added
- ✅ ProGuard rules configured
- ✅ Basic unit tests added
- ✅ Pushed to Gitea phase1-dev branch
---
## Task 2: Key Generation + Storage ✅ COMPLETE
### Completed
- ✅ **CardanoNetworkConfig.kt** - Single object for testnet/mainnet config swap
- Currently configured for TESTNET (preprod)
- Change `NETWORK` to `CardanoNetwork.MAINNET` for production
- All derived values (Koios URL, explorer URL, address prefix) auto-switch
- ✅ **CardanoKeyStorage** (interface + implementation)
- Per-session wallet isolation (key alias: `cardano_wallet_{sessionId}`)
- 24-word BIP-39 mnemonic generation using cardano-client-lib
- AES-GCM-256 encryption with Android Keystore-backed key
- `setUserAuthenticationRequired(true)` - biometric/PIN for every operation
- `setUserAuthenticationValidityDurationSeconds(-1)` - no grace period
- `setInvalidatedByBiometricEnrollment(true)` - invalidate on biometric change
- Methods: `generateWallet`, `importWallet`, `getMnemonic`, `getBaseAddress`, `getStakeAddress`, `deleteWallet`
- ✅ **CardanoWalletManager** (interface + implementation)
- Key derivation using CIP-1852 via cardano-client-lib's Account class
- Path `m/1852'/1815'/0'/0/0` for external receiving address
- Path `m/1852'/1815'/0'/2/0` for staking key
- Shelley base address generation (payment + staking key hash)
- Uses CardanoNetworkConfig for network selection
- Exposes: `getAddress(sessionId)`, `getStakeAddress(sessionId)`, `getSpendingKey(sessionId)`
- ✅ **SeedPhraseManager** (interface + implementation)
- 24-word mnemonic generation (256-bit entropy)
- Support for 12/15/18/21/24 word counts
- BIP-39 validation (checksum + wordlist)
- Word suggestions for autocomplete
- Normalization (whitespace, case)
- ⚠️ UI must apply `FLAG_SECURE` when displaying seed phrases (documented)
- ✅ **FakeCardanoKeyStorage** for testing
- ✅ Unit tests for SeedPhraseManager, CardanoNetworkConfig, CardanoWalletManager
### Decisions Made (per instructions)
- Wallet scope: **PER SESSION** (each Matrix account has its own wallet)
- Biometric change: **INVALIDATE** key + require wallet re-import/creation
- Network: **TESTNET** (preprod) - single config constant for easy mainnet swap
### Not Verified (No Android SDK in build environment)
- ⚠️ Compilation with `./gradlew :features:wallet:impl:assemble`
- ⚠️ Unit tests with `./gradlew :features:wallet:impl:test`
- ⚠️ ktlint compliance
- ⚠️ Actual Android Keystore behavior (requires device/emulator)
- ⚠️ Biometric prompt integration (requires Activity context)
### Security Notes
1. **Mnemonic never stored in plaintext** - Always encrypted with Keystore key
2. **Key material cleared after use** - `ByteArray.fill(0)` called where possible
3. **Per-session isolation** - Different Matrix accounts cannot access each other's wallets
4. **Biometric invalidation** - If user adds/removes fingerprints, wallet key becomes invalid
5. **No screenshots** - UI must apply FLAG_SECURE when showing seed phrase
---
## Task 3: Koios Client ✅ COMPLETE
### Completed
- ✅ **CardanoClient.kt** interface in `api/` module:
- `getBalance(address: String): Result<Long>` — balance in lovelace
- `getUtxos(address: String): Result<List<Utxo>>` — unspent outputs
- `submitTx(signedTxCbor: String): Result<String>` — returns tx hash
- `getTxStatus(txHash: String): Result<TxStatus>` — PENDING/CONFIRMED/FAILED
- ✅ **Data models** in `api/`:
- `Utxo.kt` — txHash, outputIndex, amount, address
- `TxStatus.kt` — enum PENDING/CONFIRMED/FAILED
- `CardanoException.kt` — typed exceptions (NetworkException, RateLimitException, InvalidAddressException, TransactionNotFoundException, SubmissionFailedException, InsufficientFundsException, ApiException)
- ✅ **KoiosCardanoClient.kt** implementation:
- Uses `BackendFactory.getKoiosBackendService()` from cardano-client-lib
- Testnet URL: `https://preprod.koios.rest/api/v1` (via CardanoNetworkConfig)
- Mainnet URL: `https://api.koios.rest/api/v1` (via CardanoNetworkConfig)
- 3 retries with exponential backoff (1s → 2s → 4s, max 10s)
- Basic rate limiting (100ms min between requests for Koios 100 req/10s limit)
- DI: `@ContributesBinding(SessionScope::class)`
- Error parsing: 429 → RateLimitException, 5xx → NetworkException, etc.
- ✅ **FakeCardanoClient.kt** for testing:
- Configurable balances, UTxOs, transaction statuses
- Error simulation (network errors, rate limits, submit failures)
- Transaction lifecycle simulation (pending → confirmed → failed)
- Call counters for test verification
- Helper: `setupWallet(address, balance)` creates realistic UTxO set
- ✅ **KoiosCardanoClientTest.kt** — 15+ unit tests:
- getBalance success, unknown address, network error, rate limit
- getUtxos success, empty result
- submitTx success, failure
- getTxStatus pending, confirmed, failed
- reset/state management
- ✅ **CardanoWalletManager updated** to use CardanoClient:
- `refreshBalance()` now fetches real balance via Koios
- Updates WalletState with lovelace + formatted ADA string
### Design Notes
- **No API key required** — Koios public API is free
- **Network config centralized** — Change `CardanoNetworkConfig.NETWORK` to swap testnet/mainnet
- **Hex CBOR for submitTx** — Accepts hex-encoded signed transaction bytes
- **UTxO pagination** — Limited to first 100 UTxOs (sufficient for typical wallets)
### Potential Issues
- ⚠️ `getTxStatus` returns PENDING for unknown hashes (could be never-submitted or truly pending)
- ⚠️ Koios rate limit (100 req/10s) may need adjustment for heavy usage patterns
- ⚠️ No getProtocolParameters yet (needed for Task 4 fee calculation)
---
## Task 4-6: See PHASE1-PLAN.md
---
## Task 7: Timeline Payment Card ✅ COMPLETE
### Completed
- ✅ **PaymentCardStatus.kt** — Enum for PENDING/CONFIRMED/FAILED states
- ✅ **TimelineItemPaymentContent.kt** — Data class implementing TimelineItemEventContent
- amountLovelace, addresses, txHash, status, network, isSentByMe
- Computed properties: amountAda, isTestnet, truncatedTxHash, explorerUrl
- Companion formatAda() helper
- ✅ **TimelineItemPaymentView.kt** — Compose UI for payment card
- Cardano icon (₳ symbol)
- Amount in ADA (formatted from lovelace)
- Status chip with spinner (pending), checkmark (confirmed), X (failed)
- Testnet badge when applicable
- Truncated tx hash (tappable → CardanoScan)
- View on explorer link for confirmed transactions
- @PreviewsDayNight with multiple preview states
- ✅ **TimelineItemPaymentContentTest.kt** — Unit tests for content model
- ✅ **Integration with TimelineItemEventContentView.kt**
### Design Notes
- Payment cards use different colors for sent (primary) vs received (surface)
- Explorer URLs: preprod.cardanoscan.io for testnet, cardanoscan.io for mainnet
- Tx hash truncated to first 8 + last 8 chars for display
---
## Task 8: Raw Event Handling ✅ COMPLETE (UPGRADED)
### ✅ RESOLVED: SDK Raw Event API
**Previous blocker:** Matrix Rust SDK did not expose raw event sending or raw JSON access.
**Resolution:** The SDK (version 26.03.24) now provides:
- `Timeline.sendRaw(eventType: String, content: String)` — Sends custom event types
- `MsgLikeKind.Other` with `eventType` field — Receives custom events
- `TimelineItemDebugInfo.originalJson` — Access to raw event JSON via debug info provider
**Implementation updated to use proper raw events instead of text markers.**
### Completed
- ✅ **PaymentEventSender.kt** — Interface for sending payment events
- ✅ **DefaultPaymentEventSender.kt** — Implementation using raw events
- Uses `timeline.sendRaw(eventType, content)` to send custom events
- Event type: `co.sulkta.payment.request` (reverse-domain format)
- Status updates: `co.sulkta.payment.status`
- No text marker hack — proper Matrix custom events
- ✅ **TimelineItemContentPaymentFactory.kt** — Parser for payment events
- `isPaymentEventType(eventType)` — Checks for payment event type
- `isStatusUpdateEventType(eventType)` — Checks for status update type
- `createFromRaw(json, isSentByMe)` — Parses raw JSON from custom events
- Supports both camelCase and snake_case field names
- Graceful error handling — returns null on malformed JSON
- ✅ **TimelineEventContentMapper.kt** — Maps `MsgLikeKind.Other` to `CustomEventContent`
- ✅ **TimelineItemContentFactory.kt** — Handles `CustomEventContent` for payments
- Gets raw JSON via `timelineItemDebugInfoProvider().originalJson`
- Delegates to paymentFactory for payment event types
- ✅ **CustomEventContent.kt** — New EventContent type for custom events
- ✅ **Timeline.sendRaw()** — Added to Timeline interface and RustTimeline implementation
- ✅ **FakePaymentEventSender.kt** — Test fake
- ✅ **TimelineItemContentPaymentFactoryTest.kt** — Updated unit tests
### m.replace Status Updates
**Decision:** Status updates are sent as separate events of type `co.sulkta.payment.status`.
**Future improvement:** When SDK exposes event relations, refactor to use m.replace for cleaner status update thread.
### Benefits of Raw Event Approach
- ✅ Proper Matrix protocol compliance (custom event types, not hacked text)
- ✅ Non-wallet clients see "Unknown event" instead of JSON-in-text
- ✅ Clean separation of payment events from regular messages
- ✅ Events won't be indexed by message search
- ✅ No message length limits concern
---
## Known Issues
### Issue 1: Biometric Prompt Activity Context
The `CardanoKeyStorageImpl` uses `setUserAuthenticationRequired(true)` which will cause `UserNotAuthenticatedException` when accessing the key. The biometric prompt UI must be triggered from an Activity/Fragment context before calling `getMnemonic()`, `getSpendingKey()`, etc.
**Solution:** Task 6 (Payment Flow UI) must call BiometricPrompt before invoking storage operations.
### Issue 2: KeyPermanentlyInvalidatedException
If user changes biometric enrollment, the Keystore key is invalidated. Current behavior: throws exception, user must delete and recreate wallet.
**Enhancement (future):** Show user-friendly message explaining why wallet became invalid and offer to re-import.
---
*Last updated: 2026-03-27 - Task 2 complete*

879
CARDANO-SPECS-REVIEW.md Normal file
View file

@ -0,0 +1,879 @@
# Cardano Technical Specifications Review for Element X ADA Wallet
**Date:** 2026-03-26
**Purpose:** Deep technical review for building a native Cardano wallet inside an Android app (Element X fork)
---
## Table of Contents
1. [Key Derivation (CIP-1852)](#1-key-derivation-cip-1852)
2. [Transaction Building & Signing](#2-transaction-building--signing)
3. [Android Library Options](#3-android-library-options)
4. [Minimum Viable TX Flow](#4-minimum-viable-tx-flow)
5. [CIP-0013: Payment URI Format](#5-cip-0013-payment-uri-format)
6. [CIP-0030: dApp-Wallet Bridge](#6-cip-0030-dapp-wallet-bridge)
7. [CIP-0045: WebRTC dApp-Wallet Communication](#7-cip-0045-webrtc-dapp-wallet-communication)
8. [WalletConnect v2 on Cardano](#8-walletconnect-v2-on-cardano)
9. [Backend APIs: Koios vs Blockfrost](#9-backend-apis-koios-vs-blockfrost)
10. [Recommendations](#10-recommendations)
---
## 1. Key Derivation (CIP-1852)
### Standard Derivation Path
CIP-1852 defines the HD wallet derivation for Cardano Shelley-era wallets, based on BIP-44/BIP-32-Ed25519.
**Standard path:**
```
m / purpose' / coin_type' / account' / role / index
```
**Concrete values:**
- `purpose`: **1852'** (hardened) - Shelley wallets (or 44' for Byron)
- `coin_type`: **1815'** (hardened) - Cardano's registered coin type
- `account`: **0'** (hardened) - First account
- `role`:
- **0** = External chain (receiving addresses)
- **1** = Internal chain (change addresses)
- **2** = Staking key
- `index`: Sequential address index
**Example derivation paths:**
```
m/1852'/1815'/0'/0/0 → First external/receiving address
m/1852'/1815'/0'/1/0 → First internal/change address
m/1852'/1815'/0'/2/0 → Staking key
```
### Code Example (cardano-serialization-lib / JavaScript)
```javascript
function harden(num) {
return 0x80000000 + num;
}
// From mnemonic to root key
import { mnemonicToEntropy } from 'bip39';
const entropy = mnemonicToEntropy(
"test walk nut penalty hip pave soap entry language right filter choice"
);
const rootKey = CardanoWasm.Bip32PrivateKey.from_bip39_entropy(
Buffer.from(entropy, 'hex'),
Buffer.from(''), // empty password
);
// Derive account key
const accountKey = rootKey
.derive(harden(1852)) // purpose
.derive(harden(1815)) // coin type
.derive(harden(0)); // account #0
// Derive payment key (external, index 0)
const utxoPubKey = accountKey
.derive(0) // external chain
.derive(0) // index 0
.to_public();
// Derive staking key
const stakeKey = accountKey
.derive(2) // staking role
.derive(0)
.to_public();
// Create base address (payment + staking)
const baseAddr = CardanoWasm.BaseAddress.new(
CardanoWasm.NetworkInfo.mainnet().network_id(),
CardanoWasm.StakeCredential.from_keyhash(utxoPubKey.to_raw_key().hash()),
CardanoWasm.StakeCredential.from_keyhash(stakeKey.to_raw_key().hash()),
);
console.log(baseAddr.to_address().to_bech32());
// Output: addr1q...
```
### Code Example (cardano-client-lib / Java/Kotlin)
```kotlin
import com.bloxbean.cardano.client.account.Account
import com.bloxbean.cardano.client.common.model.Networks
// Create new wallet from mnemonic
val mnemonic = "test walk nut penalty hip pave soap entry language right filter choice"
val account = Account(Networks.mainnet(), mnemonic)
// Get addresses
val baseAddress = account.baseAddress() // addr1q...
val enterpriseAddress = account.enterpriseAddress()
val stakeAddress = account.stakeAddress() // stake1u...
// For derivation at specific index
val account5 = Account(Networks.mainnet(), mnemonic, 5) // account index 5
```
---
## 2. Transaction Building & Signing
### Simple ADA Transfer with cardano-client-lib (Java/Kotlin)
This is the **recommended approach for Android** as it's pure Java/Kotlin.
```kotlin
import com.bloxbean.cardano.client.account.Account
import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService
import com.bloxbean.cardano.client.common.model.Networks
import com.bloxbean.cardano.client.quicktx.QuickTxBuilder
import com.bloxbean.cardano.client.quicktx.Tx
import com.bloxbean.cardano.client.function.helper.SignerProviders
// Setup
val backendService = BFBackendService(
"https://cardano-mainnet.blockfrost.io/api/v0",
"<PROJECT_ID>"
)
val sender = Account(Networks.mainnet(), "<mnemonic>")
val senderAddress = sender.baseAddress()
val receiverAddress = "addr1q..."
// Build and submit transaction
val tx = Tx()
.payToAddress(receiverAddress, Amount.ada(5.0))
.from(senderAddress)
val quickTxBuilder = QuickTxBuilder(backendService)
val result = quickTxBuilder
.compose(tx)
.withSigner(SignerProviders.signerFrom(sender))
.completeAndWait { txHash -> println("Submitted: $txHash") }
if (result.isSuccessful) {
println("Transaction hash: ${result.value}")
}
```
### Low-level Transaction Building (cardano-client-lib)
```kotlin
import com.bloxbean.cardano.client.function.TxBuilder
import com.bloxbean.cardano.client.function.TxBuilderContext
import com.bloxbean.cardano.client.function.Output
import com.bloxbean.cardano.client.function.helper.*
import com.bloxbean.cardano.client.backend.api.DefaultUtxoSupplier
import com.bloxbean.cardano.client.backend.api.DefaultProtocolParamsSupplier
// Define outputs
val output = Output.builder()
.address(receiverAddress)
.assetName("lovelace")
.qty(5_000_000) // 5 ADA in lovelace
.build()
// Build transaction
val txBuilder = output.outputBuilder()
.buildInputs(InputBuilders.createFromSender(senderAddress, senderAddress))
.andThen(BalanceTxBuilders.balanceTx(senderAddress, 1))
val utxoSupplier = DefaultUtxoSupplier(backendService.utxoService)
val protocolParamsSupplier = DefaultProtocolParamsSupplier(backendService.epochService)
// Build and sign
val signedTx = TxBuilderContext.init(utxoSupplier, protocolParamsSupplier)
.buildAndSign(txBuilder, SignerProviders.signerFrom(sender))
// Submit
val result = backendService.transactionService.submitTransaction(signedTx.serialize())
```
### Transaction Building with cardano-serialization-lib (JavaScript/WASM)
```javascript
// Protocol parameters (these change - fetch from backend)
const linearFee = CardanoWasm.LinearFee.new(
CardanoWasm.BigNum.from_str('44'),
CardanoWasm.BigNum.from_str('155381')
);
const txBuilderCfg = CardanoWasm.TransactionBuilderConfigBuilder.new()
.fee_algo(linearFee)
.pool_deposit(CardanoWasm.BigNum.from_str('500000000'))
.key_deposit(CardanoWasm.BigNum.from_str('2000000'))
.max_value_size(4000)
.max_tx_size(8000)
.coins_per_utxo_word(CardanoWasm.BigNum.from_str('34482'))
.build();
const txBuilder = CardanoWasm.TransactionBuilder.new(txBuilderCfg);
// Add input (must provide UTxO details from backend)
txBuilder.add_key_input(
prvKey.to_public().hash(),
CardanoWasm.TransactionInput.new(
CardanoWasm.TransactionHash.from_bytes(
Buffer.from("8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec", "hex")
),
0 // index
),
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str('10000000'))
);
// Add output
const outputAddr = CardanoWasm.Address.from_bech32("addr_test1qpu...");
txBuilder.add_output(
CardanoWasm.TransactionOutput.new(
outputAddr,
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str('5000000'))
)
);
// Set TTL (time-to-live)
txBuilder.set_ttl(410021);
// Add change output (calculates fee automatically)
const changeAddr = CardanoWasm.Address.from_bech32("addr_test1...");
txBuilder.add_change_if_needed(changeAddr);
// Build transaction body
const txBody = txBuilder.build();
const txHash = CardanoWasm.hash_transaction(txBody);
// Sign
const witnesses = CardanoWasm.TransactionWitnessSet.new();
const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
const vkeyWitness = CardanoWasm.make_vkey_witness(txHash, prvKey);
vkeyWitnesses.add(vkeyWitness);
witnesses.set_vkeys(vkeyWitnesses);
// Assemble final transaction
const transaction = CardanoWasm.Transaction.new(txBody, witnesses, undefined);
const signedTxCbor = transaction.to_bytes();
```
---
## 3. Android Library Options
### Option A: cardano-client-lib (RECOMMENDED)
**Repository:** https://github.com/bloxbean/cardano-client-lib
**Why it's the best choice for Android:**
- **Pure Java** - No JNI/native dependencies
- **Works on Android out of the box** - Min SDK 21+
- **Active development** - Regular releases, good documentation
- **Modular design** - Include only what you need
- **Backend-agnostic** - Supports Blockfrost, Koios, Ogmios/Kupo
- **CIP implementations** - CIP-8 (message signing), CIP-20, CIP-25, CIP-30, CIP-67, CIP-68
**Gradle dependencies:**
```groovy
dependencies {
// Core library
implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.1'
// Pick ONE backend:
implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.7.1'
// OR
implementation 'com.bloxbean.cardano:cardano-client-backend-koios:0.7.1'
}
```
**Key modules:**
| Module | Purpose |
|--------|---------|
| `cardano-client-crypto` | BIP32, BIP39, CIP1852 key derivation |
| `cardano-client-address` | Address generation (Base, Enterprise, Stake) |
| `cardano-client-transaction-spec` | Transaction serialization (CBOR) |
| `cardano-client-quicktx` | High-level transaction builder |
| `cardano-client-cip30` | dApp-Wallet bridge implementation |
### Option B: cardano-serialization-lib (CSL)
**Repository:** https://github.com/Emurgo/cardano-serialization-lib
**Android challenges:**
- Written in **Rust**, compiled to WebAssembly
- For Android, requires **JNI bindings** via Rust FFI
- React Native wrapper exists (`react-native-haskell-shelley`) but adds complexity
- **No official AAR** published
- Would need to compile Rust to Android NDK targets (arm64-v8a, armeabi-v7a, x86, x86_64)
**When to consider:**
- If you need exact byte-level compatibility with other CSL implementations
- If building a complex dApp that needs Plutus script support
- If your team already has Rust/JNI expertise
**Build requirements for Android:**
```bash
# Would need to set up:
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
# Then build with cargo-ndk or similar
```
### Option C: Blockfrost Kotlin SDK
**Repository:** https://github.com/blockfrost/blockfrost-kotlin
**Purpose:** API client only - does NOT handle key management or transaction building
**Use for:** Querying blockchain data, submitting pre-built transactions
```kotlin
import io.blockfrost.sdk_kotlin.api.CardanoAddressesApi
import io.blockfrost.sdk_kotlin.infrastructure.ApiClient
ApiClient.apiKey["project_id"] = "<PROJECT_ID>"
val addressApi = CardanoAddressesApi("https://cardano-mainnet.blockfrost.io/api/v0")
val utxos = addressApi.getAddressUtxos("addr1q...")
```
### Recommendation Matrix
| Use Case | Recommended Library |
|----------|---------------------|
| Simple wallet (send/receive ADA) | cardano-client-lib |
| NFT minting | cardano-client-lib |
| Staking delegation | cardano-client-lib |
| Complex smart contracts | cardano-serialization-lib (with significant integration effort) |
| Just querying data | Blockfrost Kotlin SDK |
---
## 4. Minimum Viable TX Flow
### High-Level Flow
```
1. USER INITIATES PAYMENT
└─> Parse recipient address, amount
2. FETCH UTXOs
└─> Query backend for sender's unspent outputs
└─> Select UTxOs covering amount + fees
3. BUILD TRANSACTION
├─> Inputs: Selected UTxOs
├─> Outputs: Payment output + change output
├─> Calculate fees based on tx size
└─> Set TTL (current slot + ~7200 slots = ~2 hours)
4. SIGN TRANSACTION
└─> Use payment private key to create witness
5. SUBMIT TRANSACTION
└─> POST serialized CBOR to backend
6. CONFIRM
└─> Poll for confirmation or use websocket
```
### Kotlin Implementation
```kotlin
class CardanoWallet(
private val backendService: BackendService,
private val account: Account
) {
suspend fun sendAda(
recipientAddress: String,
amountAda: Double
): Result<String> = withContext(Dispatchers.IO) {
try {
val tx = Tx()
.payToAddress(recipientAddress, Amount.ada(amountAda))
.from(account.baseAddress())
val result = QuickTxBuilder(backendService)
.compose(tx)
.withSigner(SignerProviders.signerFrom(account))
.complete()
if (result.isSuccessful) {
// Submit
val submitResult = backendService.transactionService
.submitTransaction(result.value.serialize())
if (submitResult.isSuccessful) {
Result.success(submitResult.value)
} else {
Result.failure(Exception(submitResult.response))
}
} else {
Result.failure(Exception("Failed to build transaction"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
```
### Fee Calculation
Cardano fees are calculated as:
```
fee = a * tx_size_bytes + b
```
Current mainnet parameters (as of March 2026):
- `a` = 44 lovelace/byte
- `b` = 155,381 lovelace (base fee)
Typical simple transaction: ~250-350 bytes → ~165,000-175,000 lovelace (~0.17 ADA)
---
## 5. CIP-0013: Payment URI Format
### URI Format
```
web+cardano:<address>[?amount=<amount>][&message=<message>]
```
### Parameters
| Parameter | Required | Description |
|-----------|----------|-------------|
| `address` | Yes | Bech32 address (addr1...) or $handle |
| `amount` | No | Amount in lovelace (1 ADA = 1,000,000 lovelace) |
| `message` | No | URL-encoded message (for tx metadata) |
### Examples
```
# Simple payment request
web+cardano:addr1qxck...
# With amount (5 ADA = 5,000,000 lovelace)
web+cardano:addr1qxck...?amount=5000000
# With amount and message
web+cardano:addr1qxck...?amount=5000000&message=Coffee%20payment
# Using ADA handle
web+cardano:$kayos
```
### Kotlin Parser
```kotlin
data class CardanoPaymentRequest(
val address: String,
val amount: Long? = null, // in lovelace
val message: String? = null
)
fun parseCardanoUri(uri: String): CardanoPaymentRequest? {
val prefix = "web+cardano:"
if (!uri.startsWith(prefix)) return null
val withoutPrefix = uri.removePrefix(prefix)
val parts = withoutPrefix.split("?", limit = 2)
val address = parts[0]
val params = if (parts.size > 1) {
parts[1].split("&").associate { param ->
val kv = param.split("=", limit = 2)
kv[0] to (if (kv.size > 1) URLDecoder.decode(kv[1], "UTF-8") else "")
}
} else emptyMap()
return CardanoPaymentRequest(
address = address,
amount = params["amount"]?.toLongOrNull(),
message = params["message"]
)
}
// Usage
val request = parseCardanoUri("web+cardano:addr1q...?amount=5000000&message=Test")
// CardanoPaymentRequest(address="addr1q...", amount=5000000, message="Test")
```
### Android Intent Filter
```xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="web+cardano" />
</intent-filter>
```
---
## 6. CIP-0030: dApp-Wallet Bridge
### Overview
CIP-0030 defines a JavaScript API for web dApps to communicate with browser extension wallets. For a mobile wallet, this is relevant when:
- Implementing WalletConnect-style connectivity
- Building an in-app dApp browser
- Providing SDK for dApps to integrate
### API Methods
**Initialization:**
```javascript
// dApp requests connection
const api = await window.cardano.nami.enable();
```
**Key Methods:**
| Method | Returns | Description |
|--------|---------|-------------|
| `getNetworkId()` | `Promise<number>` | 0 = testnet, 1 = mainnet |
| `getUtxos()` | `Promise<TransactionUnspentOutput[]>` | Wallet's UTxOs in CBOR |
| `getBalance()` | `Promise<Value>` | Total balance (CBOR) |
| `getUsedAddresses()` | `Promise<Address[]>` | Used addresses |
| `getUnusedAddresses()` | `Promise<Address[]>` | Fresh addresses |
| `getChangeAddress()` | `Promise<Address>` | Address for change |
| `getRewardAddresses()` | `Promise<Address[]>` | Staking addresses |
| `signTx(tx, partialSign)` | `Promise<TransactionWitnessSet>` | Sign transaction |
| `signData(addr, payload)` | `Promise<DataSignature>` | CIP-8 message signing |
| `submitTx(tx)` | `Promise<TransactionHash>` | Submit to network |
### cardano-client-lib CIP-30 Support
```kotlin
import com.bloxbean.cardano.client.cip.cip30.CIP30DataSigner
// Sign arbitrary data (CIP-8)
val signature = CIP30DataSigner.sign(
account.hdKeyPair().privateKey,
"Hello Cardano!".toByteArray()
)
// Returns: { signature: "...", key: "..." }
```
---
## 7. CIP-0045: WebRTC dApp-Wallet Communication
### Overview
CIP-0045 enables **P2P communication** between dApps and mobile wallets using WebRTC/WebTorrent, eliminating the need for browser extensions.
### How It Works
```
┌─────────────┐ WebRTC/WebTorrent ┌──────────────┐
│ dApp │ <─────────────────────────────────>│ Mobile Wallet│
│ (Browser) │ P2P via BitTorrent trackers │ (App) │
└─────────────┘ └──────────────┘
1. dApp displays QR code
2. Wallet scans, establishes P2P
3. CIP-30 API over the channel
```
### Reference Implementation
**Library:** `@fabianbormann/cardano-peer-connect`
**Repository:** https://github.com/fabianbormann/cardano-peer-connect
### Wallet Implementation (TypeScript/React Native)
```typescript
import { CardanoPeerConnect } from '@fabianbormann/cardano-peer-connect';
class MyWalletPeerConnect extends CardanoPeerConnect {
constructor() {
super({
name: 'Element X ADA',
version: '1.0.0',
icon: '<base64-icon>'
});
}
// Implement CIP-30 methods
async getRewardAddresses(): Promise<string[]> {
return [this.wallet.stakeAddress];
}
async getUsedAddresses(): Promise<string[]> {
return [this.wallet.baseAddress];
}
async signTx(tx: string, partialSign: boolean): Promise<string> {
// Show UI for user approval
const approved = await this.showSigningDialog(tx);
if (!approved) throw new Error('User rejected');
return this.wallet.sign(tx);
}
}
// Connect to dApp
const peerConnect = new MyWalletPeerConnect();
peerConnect.setOnConnect((message) => {
console.log('Connected to dApp:', message.dApp.name);
});
// Scan QR code to get dApp identifier
const dAppId = 'bYUh6Bn6A...388LR1JCrED';
peerConnect.connect(dAppId, [
'wss://tracker.openwebtorrent.com:443/announce',
'wss://tracker.btorrent.xyz'
]);
```
### Android Wallets Supporting CIP-0045
| Wallet | CIP-0045 Support | Notes |
|--------|-----------------|-------|
| Eternl | ✅ | Full support |
| Typhon | ✅ | Full support |
| Vespr | ⚠️ | WalletConnect preferred |
| Nami | ❌ | Browser extension only |
| Lace | ❌ | Browser extension only |
### Recommendation
CIP-0045 is powerful but complex. For MVP:
1. **Start without CIP-0045** - focus on send/receive
2. **Phase 2:** Add CIP-0013 URI handling
3. **Phase 3:** Consider CIP-0045 for dApp integration
---
## 8. WalletConnect v2 on Cardano
### Current State
WalletConnect v2 on Cardano is **fragmented**:
- **No official Cardano namespace** in WC2 registry
- Different wallets use different approaches:
- **VESPR**: Uses custom implementation
- **Flint**: Has WC2 support
- **Most wallets**: Prefer CIP-0045
### VESPR Wallet
VESPR (https://vespr.xyz/) is a popular mobile wallet:
- **Android:** Yes (Play Store)
- **iOS:** Yes (App Store)
- Supports both CIP-0045 and a custom WC-style protocol
- Closed source
### For Element X ADA
**Recommendation:** Implement CIP-0045 rather than WalletConnect v2:
- Better Cardano ecosystem adoption
- Open standard with reference implementation
- Works with more dApps
---
## 9. Backend APIs: Koios vs Blockfrost
### Feature Comparison
| Feature | Blockfrost | Koios |
|---------|------------|-------|
| **Pricing** | Freemium (50k req/day free) | Free (community) |
| **Rate Limits** | 10 req/sec, 500 burst | 100 req/10s |
| **Decentralization** | Centralized | Decentralized nodes |
| **Kotlin SDK** | ✅ Official | ❌ (REST only) |
| **Java SDK** | ✅ Official | Via cardano-client-lib |
| **Self-hosting** | ❌ | ✅ |
| **IPFS** | ✅ | ❌ |
| **Reliability** | Very high | Good (varies by node) |
### API Comparison
**Get UTxOs:**
```bash
# Blockfrost
curl -H "project_id: <ID>" \
"https://cardano-mainnet.blockfrost.io/api/v0/addresses/addr1.../utxos"
# Koios
curl "https://api.koios.rest/api/v1/address_utxos?_address=addr1..."
```
**Submit Transaction:**
```bash
# Blockfrost
curl -X POST -H "project_id: <ID>" \
-H "Content-Type: application/cbor" \
--data-binary @tx.signed \
"https://cardano-mainnet.blockfrost.io/api/v0/tx/submit"
# Koios
curl -X POST \
-H "Content-Type: application/cbor" \
--data-binary @tx.signed \
"https://api.koios.rest/api/v1/submittx"
```
### Kotlin/Android Usage
**Blockfrost with cardano-client-lib:**
```kotlin
val backendService = BFBackendService(
Constants.BLOCKFROST_MAINNET_URL,
"<PROJECT_ID>"
)
val utxos = backendService.utxoService.getUtxos(address, 100, 1)
```
**Koios with cardano-client-lib:**
```kotlin
val backendService = KoiosBackendService(
Constants.KOIOS_MAINNET_URL
)
val utxos = backendService.utxoService.getUtxos(address, 100, 1)
```
### Recommendation for Element X ADA
**Use Blockfrost** for MVP:
- Official Kotlin SDK
- Higher reliability guarantees
- Free tier sufficient for development/testing
- Easy to switch to Koios later (same cardano-client-lib interface)
**Consider Koios** for production if:
- Need to minimize costs at scale
- Want decentralization
- Plan to self-host infrastructure
---
## 10. Recommendations
### Architecture Summary
```
┌─────────────────────────────────────────────────────────┐
│ Element X ADA │
├─────────────────────────────────────────────────────────┤
│ UI Layer (Compose) │
│ ├─ WalletScreen (balance, history) │
│ ├─ SendScreen (CIP-0013 URI parsing) │
│ └─ ReceiveScreen (QR generation) │
├─────────────────────────────────────────────────────────┤
│ Wallet Core │
│ ├─ KeyManager (encrypted mnemonic storage) │
│ ├─ TransactionBuilder (QuickTx API) │
│ └─ AddressGenerator (CIP-1852) │
├─────────────────────────────────────────────────────────┤
│ cardano-client-lib (0.7.1) │
│ ├─ cardano-client-lib │
│ ├─ cardano-client-backend-blockfrost │
│ └─ cardano-client-cip30 (for future dApp support) │
├─────────────────────────────────────────────────────────┤
│ Backend │
│ └─ Blockfrost API (mainnet/testnet) │
└─────────────────────────────────────────────────────────┘
```
### MVP Feature Set
1. **Wallet Management**
- Generate new wallet (24-word mnemonic)
- Import existing wallet
- Secure storage (Android Keystore)
2. **Basic Operations**
- View ADA balance
- View transaction history
- Send ADA (manual entry)
- Receive ADA (QR code + address)
3. **CIP Support (Phase 1)**
- CIP-1852: Key derivation ✅
- CIP-0013: Payment URIs ✅
4. **Future Enhancements (Phase 2+)**
- Native tokens display
- Staking delegation
- CIP-0045: dApp connectivity
- NFT gallery
### Dependencies (build.gradle)
```groovy
dependencies {
// Cardano core
implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.1'
implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.7.1'
// For CIP-30 support (Phase 2)
implementation 'com.bloxbean.cardano:cardano-client-cip30:0.7.1'
// Security
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
// QR codes
implementation 'com.google.zxing:core:3.5.2'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
}
```
### Security Considerations
1. **Mnemonic Storage:**
- Use Android Keystore + EncryptedSharedPreferences
- Never store plaintext
- Consider hardware-backed keys on supported devices
2. **Transaction Signing:**
- Derive keys in memory only
- Clear sensitive data after use
- Require biometric/PIN for signing
3. **Network:**
- Use certificate pinning for API calls
- Validate all addresses before sending
---
## Appendix: Quick Reference
### Address Prefixes
| Type | Mainnet Prefix | Testnet Prefix |
|------|---------------|----------------|
| Base | addr1 | addr_test1 |
| Enterprise | addr1 | addr_test1 |
| Stake | stake1 | stake_test1 |
| Script | addr1 | addr_test1 |
### Lovelace Conversions
```
1 ADA = 1,000,000 lovelace
1 lovelace = 0.000001 ADA
// Kotlin helper
fun adaToLovelace(ada: Double): Long = (ada * 1_000_000).toLong()
fun lovelaceToAda(lovelace: Long): Double = lovelace / 1_000_000.0
```
### Testnet Faucet
https://docs.cardano.org/cardano-testnets/tools/faucet/
---
*Document prepared for Element X ADA project. Last updated: 2026-03-26*

3745
CHANGES.md

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
@AGENTS.md

View file

@ -1 +0,0 @@
* @element-hq/element-x-android-reviewers

View file

@ -1,204 +0,0 @@
# Contributing to Element X Android
<!--- TOC -->
* [Developer onboarding](#developer-onboarding)
* [Contributing code to Matrix](#contributing-code-to-matrix)
* [Android Studio settings](#android-studio-settings)
* [Compilation](#compilation)
* [Strings](#strings)
* [I want to add new strings to the project](#i-want-to-add-new-strings-to-the-project)
* [I want to help translating Element](#i-want-to-help-translating-element)
* [Element X Android Gallery](#element-x-android-gallery)
* [I want to add a new feature to Element X Android](#i-want-to-add-a-new-feature-to-element-x-android)
* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue)
* [Kotlin](#kotlin)
* [Changelog](#changelog)
* [Code quality](#code-quality)
* [detekt](#detekt)
* [ktlint](#ktlint)
* [checkDocs](#checkdocs)
* [lint](#lint)
* [Unit tests](#unit-tests)
* [konsist](#konsist)
* [Tests](#tests)
* [Accessibility](#accessibility)
* [Jetpack Compose](#jetpack-compose)
* [Authors](#authors)
* [Thanks](#thanks)
<!--- END -->
## Developer onboarding
For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md).
## Contributing code to Matrix
If instead of contributing to the Element X Android project, you want to contribute to Synapse, the homeserver implementation, please read the [Synapse contribution guide](https://element-hq.github.io/synapse/latest/development/contributing_guide.html).
Element X Android support can be found in this room: [![Element X Android Matrix room #element-x-android:matrix.org](https://img.shields.io/matrix/element-x-android:matrix.org.svg?label=%23element-x-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-x-android:matrix.org).
The rest of the document contains specific rules for Matrix Android projects.
## Android Studio settings
Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`).
Please ensure that you're using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them.
## Compilation
This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`.
## Strings
The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS.
### I want to add new strings to the project
Only the core team can modify or add English strings to Localazy. As an external contributor, if you want to add new strings, feel free to add an Android resource file to the project (for instance a file named `temporary.xml`), with a note in the description of the PR for the reviewer to integrate the String into `Localazy`. If accepted, the reviewer will add the String(s) for you, and then you can download them on your branch (following these [instructions](./tools/localazy/README.md#download-translations)) and remove the temporary file.
Please follow the naming rules for the key. More details in [the dedicated section in this README.md](./tools/localazy/README.md#key-naming-rules)
### I want to help translating Element
To help translating, please go to [https://localazy.com/p/element](https://localazy.com/p/element).
- If you want to fix an issue with an English string, please open an issue on the github project of Element X (Android or iOS). Only the core team can modify or add English strings.
- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please go to [https://localazy.com/p/element](https://localazy.com/p/element).
More information can be found [in this README.md](./tools/localazy/README.md).
Once a language is sufficiently translated, it will be added to the app. The core team will decide when a language is sufficiently translated.
### Element X Android Gallery
Once added to Localazy, translations can be checked screen per screen using our tool Element X Android Gallery, available at https://element-hq.github.io/element-x-android/.
Localazy syncs occur every Monday and the screenshots on this page are generated every Tuesday, so you'll have to wait to see your change appearing on Element X Android Gallery.
## I want to add a new feature to Element X Android
Thank you for contributing to the project! Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request.
Also, please keep in mind that any feature added to Element X Android needs to be added to [the iOS client](https://github.com/element-hq/element-x-ios) too, unless it's related to an Android OS only behaviour.
**IMPORTANT:** if you are adding new screens or modifying existing ones, this needs acceptance from the product and design teams before being merged. For this, it's better to start with a [feature request issue](https://github.com/element-hq/element-x-android/issues/new?template=enhancement.yml) describing the change you want to make and the motivation behind it instead of directly creating a pull request. This will allow the product and design teams to give feedback on the change before you start working on it, and avoid you doing work that might end up being rejected.
## I want to submit a PR to fix an issue
Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request.
Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it.
If an issue does not exist yet, it may be relevant to open a new issue and let us know that you're implementing it.
### Kotlin
This project is full Kotlin. Please do not write Java classes.
### Changelog
The release notes are generated from the pull request titles and labels. If possible, the title must describe best what will be the user facing change.
You will also need to add a label starting by `PR-` to you Pull Request to help categorize the release note. The label should be added by the PR author, but can be added by the reviewer if the submitter does not have right to add label. Also note that the label can be added after the PR has been merged, as soon as the release is not done yet.
### Code quality
Make sure the following commands execute without any error:
<pre>
./tools/quality/check.sh
</pre>
Some separate commands can also be run, see below.
#### detekt
<pre>
./gradlew detekt
</pre>
#### ktlint
<pre>
./gradlew ktlintCheck --continue
</pre>
Note that you can run
<pre>
./gradlew ktlintFormat
</pre>
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
#### checkDocs
`checkDocs` is a Gradle task which checks markdown files on the project to ensure their table of contents is up to date. It uses `tools/docs/generate_toc.py --verify` under the hood, and has a counterpart `generateDocsToc` task which runs `tools/docs/generate_toc.py` to update the table of contents of markdown files.
So everytime the toc should be updated, just run
<pre>
./gradlew generateDocsToc
</pre>
and commit the changes.
The CI will check that markdown files are up to date by running
<pre>
./gradlew checkDocs
</pre>
#### lint
<pre>
./gradlew lint
</pre>
### Unit tests
Make sure the following commands execute without any error:
<pre>
./gradlew test
</pre>
#### konsist
[konsist](https://github.com/LemonAppDev/konsist) is setup in the project to check that the architecture and the naming rules are followed. Konsist tests are classical Unit tests.
### Tests
Element X is currently supported on Android Marshmallow (API 23+): please test your change on an Android device (or Android emulator) running with API 23. Many issues can happen (including crashes) on older devices.
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
### Accessibility
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.
For instance, when updating the image `src` of an ImageView, please also consider updating its `contentDescription`. A good example is a play pause button.
### Jetpack Compose
When adding or editing `@Composable`, make sure that you create an internal function annotated with `@PreviewsDayNight`, with a name suffixed by `Preview`, and having `ElementPreview` as the root composable.
Example:
```kotlin
@PreviewsDayNight
@Composable
internal fun PinIconPreview() = ElementPreview {
PinIcon()
}
```
This will allow to preview the composable in both light and dark mode in Android Studio. This will also automatically add UI tests. The GitHub action [Record screenshots](https://github.com/element-hq/element-x-android/actions/workflows/recordScreenshots.yml) has to be run to record the new screenshots. The PR reviewer can trigger this for you if you're not part of the core team.
### Authors
Feel free to add an entry in file AUTHORS.md
## Thanks
Thanks for contributing to Matrix projects!

581
ELEMENT-X-ANDROID-REVIEW.md Normal file
View file

@ -0,0 +1,581 @@
# Element X Android — Cardano Wallet Integration Technical Review
**Date:** 2026-03-26
**Author:** Kayos (Technical Review Subagent)
**Target:** Integrating a built-in Cardano wallet with `/pay` slash command
---
## Executive Summary
Element X Android is well-architected for extension. The codebase uses a clean feature module pattern with clear separation between API, implementation, and test modules. Key findings:
1. **Slash commands don't exist yet**`/` is recognized but returns empty suggestions. This is a clean integration point.
2. **Android Keystore infrastructure exists**`SecretKeyRepository` is already used for crypto operations; we can extend it.
3. **Timeline rendering is pluggable** — Adding a payment card bubble is straightforward via the factory pattern.
4. **Account data APIs are NOT exposed** — The Rust SDK has these but they're not surfaced to Kotlin yet. This is the main blocker.
5. **Widget infrastructure is solid** — Element Call uses WebView + postMessage; could inform a payment confirmation UI.
**Honest Effort Estimate:** 3-4 weeks for a competent Android developer familiar with Matrix.
---
## 1. Slash Command System
### Current State
The slash command infrastructure exists but is **intentionally empty**. Element X recognizes `/` as a command trigger but returns no suggestions.
**Key Files:**
```
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Suggestion.kt
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt
```
**Suggestion.kt** defines the types:
```kotlin
sealed interface SuggestionType {
data object Mention : SuggestionType // @
data object Command : SuggestionType // /
data object Room : SuggestionType // #
data object Emoji : SuggestionType // :
data class Custom(val pattern: String) : SuggestionType
}
```
**SuggestionsProcessor.kt** handles suggestions but explicitly returns empty for commands:
```kotlin
SuggestionType.Command,
SuggestionType.Emoji,
is SuggestionType.Custom -> {
// Clear suggestions
emptyList()
}
```
### How to Add `/pay`
1. **Create a new slash command data model:**
```kotlin
// features/wallet/api/src/main/kotlin/io/.../SlashCommand.kt
sealed interface SlashCommand {
data class Pay(val recipient: UserId?, val amount: String?) : SlashCommand
// Future: /balance, /receive, etc.
}
```
2. **Extend SuggestionsProcessor to handle commands:**
```kotlin
SuggestionType.Command -> {
val commands = listOf(
ResolvedSuggestion.Command("/pay", "Send ADA to someone"),
// ... other commands
)
commands.filter { it.command.contains(suggestion.text, ignoreCase = true) }
}
```
3. **Add `ResolvedSuggestion.Command` type:**
```kotlin
// In libraries/textcomposer/impl/.../mentions/ResolvedSuggestion.kt
data class Command(val command: String, val description: String) : ResolvedSuggestion
```
4. **Intercept message sending in MessageComposerPresenter:**
```kotlin
// In sendMessage(), before sending to timeline:
val slashCommand = parseSlashCommand(message.markdown)
if (slashCommand != null) {
when (slashCommand) {
is SlashCommand.Pay -> {
// Navigate to payment flow instead of sending message
navigator.navigateToPayment(slashCommand.recipient, slashCommand.amount)
return@launch
}
}
}
```
### Files to Modify
| File | Change |
|------|--------|
| `SuggestionsProcessor.kt` | Add command filtering logic |
| `ResolvedSuggestion.kt` | Add `Command` type |
| `MessageComposerPresenter.kt` | Intercept `/pay` before send |
| New: `SlashCommandParser.kt` | Parse `/pay @user 100` syntax |
---
## 2. Key Storage (Android Keystore)
### Current Infrastructure
Element X already uses Android Keystore for AES encryption. The infrastructure is **solid and reusable**.
**Key Files:**
```
libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt
libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt
libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFile.kt
```
**SecretKeyRepository interface:**
```kotlin
interface SecretKeyRepository {
fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey
fun deleteKey(alias: String)
}
```
**KeyStoreSecretKeyRepository implementation:**
```kotlin
@ContributesBinding(AppScope::class)
class KeyStoreSecretKeyRepository(
private val keyStore: KeyStore,
) : SecretKeyRepository {
override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey {
// Uses Android Keystore with AES-GCM-128
val generator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM, ANDROID_KEYSTORE)
val keyGenSpec = KeyGenParameterSpec.Builder(alias, ...)
.setUserAuthenticationRequired(requiresUserAuthentication)
.build()
// ...
}
}
```
### What We Need to Add
1. **Cardano-specific key storage module:**
```kotlin
// libraries/wallet/impl/src/.../CardanoKeyStorage.kt
@Inject
class CardanoKeyStorage(
private val secretKeyRepository: SecretKeyRepository,
private val encryptionService: EncryptionDecryptionService,
private val sessionStore: SessionStore,
) {
private val KEY_ALIAS_PREFIX = "cardano_wallet_"
fun storeSpendingKey(sessionId: SessionId, encryptedKey: ByteArray): Result<Unit>
fun getSpendingKey(sessionId: SessionId): Result<ByteArray>
fun hasWallet(sessionId: SessionId): Boolean
fun deleteWallet(sessionId: SessionId): Result<Unit>
}
```
2. **Biometric/PIN protection option:**
```kotlin
secretKeyRepository.getOrCreateKey(
alias = "${KEY_ALIAS_PREFIX}${sessionId.value}",
requiresUserAuthentication = true // Requires biometric/PIN
)
```
3. **Leverage EncryptedFile pattern for key backup:**
```kotlin
val encryptedFile = EncryptedFile(context, walletKeyFile)
encryptedFile.openFileOutput().use { it.write(encryptedSpendingKey) }
```
### Security Considerations
- Keys should be **per-session** (each Matrix account has its own wallet)
- Consider `setUserAuthenticationRequired(true)` for spending keys
- Use existing `RandomSecretPassphraseProvider` pattern for derived keys
- **DO NOT** store raw spending keys — always encrypt with Keystore-backed key
---
## 3. Widget Infrastructure (Element Call Pattern)
### How Element Call Works
Element Call is embedded as a WebView-based widget with bidirectional message passing.
**Key Files:**
```
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
```
**Architecture:**
```
┌─────────────────────────────────────────┐
│ CallScreenView │
│ ┌───────────────────────────────────┐ │
│ │ WebView │ │
│ │ (Element Call HTML/JS) │ │
│ └───────────────────────────────────┘ │
│ ▲ │ │
│ │ postMessage │
│ ▼ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ WebViewWidgetMessageInterceptor │ │
│ └───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ MatrixWidgetDriver │ │
│ │ (SDK ↔ Widget communication) │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
**WidgetMessage structure:**
```kotlin
@Serializable
data class WidgetMessage(
@SerialName("api") val direction: Direction,
@SerialName("widgetId") val widgetId: String,
@SerialName("requestId") val requestId: String,
@SerialName("action") val action: Action,
@SerialName("data") val data: JsonElement? = null,
)
```
### Relevance for Wallet
We could use this pattern for:
1. **Payment confirmation UI** — A WebView showing tx details before signing
2. **dApp browser** — Future widget-based dApp integration
3. **Cross-device signing** — If we need to coordinate with desktop wallets
For MVP `/pay`, we probably **don't need widgets** — a native Compose UI is simpler.
---
## 4. Message Rendering (Payment Card Bubble)
### Current Architecture
Timeline items are rendered through a factory + sealed class pattern.
**Key Files:**
```
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
```
**TimelineItemEventContent** is a sealed interface with multiple implementations:
```kotlin
sealed interface TimelineItemEventContent {
val type: String
}
// Existing types:
data class TimelineItemTextBasedContent(...) : TimelineItemEventContent
data class TimelineItemImageContent(...) : TimelineItemEventContent
data class TimelineItemPollContent(...) : TimelineItemEventContent
data class TimelineItemLocationContent(...) : TimelineItemEventContent
// ... etc
```
**TimelineItemContentFactory** maps SDK `EventContent` → UI content:
```kotlin
suspend fun create(itemContent: EventContent, ...): TimelineItemEventContent {
return when (itemContent) {
is MessageContent -> messageFactory.create(...)
is PollContent -> pollFactory.create(...)
is UnknownContent -> TimelineItemUnknownContent
// ... etc
}
}
```
**TimelineItemEventContentView** dispatches rendering:
```kotlin
@Composable
fun TimelineItemEventContentView(content: TimelineItemEventContent, ...) {
when (content) {
is TimelineItemTextBasedContent -> TimelineItemTextView(...)
is TimelineItemPollContent -> TimelineItemPollView(...)
// ... etc
}
}
```
### Adding Payment Card
1. **Create content model:**
```kotlin
// features/wallet/impl/src/.../timeline/TimelineItemPaymentContent.kt
data class TimelineItemPaymentContent(
val transactionId: String?,
val senderAddress: String,
val recipientAddress: String,
val amount: String, // e.g., "100 ADA"
val status: PaymentStatus, // Pending, Confirmed, Failed
val timestamp: Long,
val txHash: String?, // Once confirmed
) : TimelineItemEventContent {
override val type: String = "m.payment" // Custom event type
}
enum class PaymentStatus { PENDING, CONFIRMED, FAILED }
```
2. **Create factory:**
```kotlin
// features/wallet/impl/src/.../timeline/TimelineItemContentPaymentFactory.kt
@Inject
class TimelineItemContentPaymentFactory {
fun create(content: PaymentEventContent): TimelineItemPaymentContent {
return TimelineItemPaymentContent(
transactionId = content.transactionId,
// ... map fields
)
}
}
```
3. **Create view:**
```kotlin
// features/wallet/impl/src/.../timeline/TimelineItemPaymentView.kt
@Composable
fun TimelineItemPaymentView(
content: TimelineItemPaymentContent,
modifier: Modifier = Modifier,
) {
Card(modifier = modifier) {
Column {
// Icon + "Payment" header
Row {
Icon(Icons.Default.Payment)
Text("Payment")
}
// Amount
Text(
text = content.amount,
style = MaterialTheme.typography.headlineMedium
)
// Status indicator
StatusChip(content.status)
// View on explorer link (if confirmed)
if (content.txHash != null) {
TextButton(onClick = { openExplorer(content.txHash) }) {
Text("View on CardanoScan")
}
}
}
}
}
```
4. **Register in TimelineItemContentFactory:**
```kotlin
// Add to existing factory
is PaymentContent -> paymentFactory.create(content)
```
5. **Register in TimelineItemEventContentView:**
```kotlin
// Add to existing when block
is TimelineItemPaymentContent -> TimelineItemPaymentView(content = content, modifier = modifier)
```
### Custom Event Type
We need a custom Matrix event type for payments. Options:
1. **`m.payment.cardano`** — Custom event type (requires SDK extension)
2. **`m.room.message` with `msgtype: "m.payment"`** — Easier but hacky
3. **Room state event** — For persistent payment records
The SDK currently handles `UnknownContent` for unrecognized events, so we'd need to:
- Either extend the Rust SDK to recognize `m.payment.*`
- Or use the raw event JSON and parse it ourselves
---
## 5. Account Data APIs
### Current State: **BLOCKER**
Account data (`m.account_data`) is the natural place to store:
- Encrypted wallet seed (per-account)
- Wallet addresses (for backup/sync)
- Payment preferences
**The Matrix Rust SDK has account data APIs, but Element X Android does NOT expose them.**
The `MatrixClient` interface has no:
- `getAccountData(type: String)`
- `setAccountData(type: String, content: JsonObject)`
- Room-level account data access
### Workarounds
1. **Local-only storage (MVP approach):**
- Store wallet keys in Android Keystore
- Store addresses in DataStore/preferences
- **Con:** No cross-device sync
2. **Use a custom room as wallet state:**
- Create a self-DM or special room
- Store state as room events
- **Con:** Awkward, discoverable
3. **Extend the SDK (proper solution):**
- Add account data bindings to `RustMatrixClient`
- Expose `setAccountData` / `getAccountData`
- **Time:** 2-3 days for SDK extension
### Recommendation
For MVP: **Local-only storage.** The user's wallet exists only on that device.
For V2: Extend SDK to use account data for encrypted seed backup.
---
## 6. Recommended File Structure
```
features/
└── wallet/
├── api/
│ └── src/main/kotlin/io/element/android/features/wallet/api/
│ ├── WalletEntryPoint.kt # Feature entry point
│ ├── WalletState.kt # Wallet balance/state
│ ├── PaymentRequest.kt # /pay parsing
│ └── slash/
│ └── SlashCommand.kt # Slash command models
├── impl/
│ └── src/main/kotlin/io/element/android/features/wallet/impl/
│ ├── di/
│ │ └── WalletModule.kt # DI bindings
│ ├── storage/
│ │ ├── CardanoKeyStorage.kt # Keystore wrapper
│ │ └── WalletPreferences.kt # DataStore prefs
│ ├── cardano/
│ │ ├── CardanoWalletManager.kt # Wallet operations
│ │ ├── TransactionBuilder.kt # Tx construction
│ │ └── BlockfrostClient.kt # Chain queries
│ ├── payment/
│ │ ├── PaymentFlowNode.kt # Navigation node
│ │ ├── PaymentPresenter.kt # MVP logic
│ │ ├── PaymentState.kt # UI state
│ │ └── PaymentView.kt # Compose UI
│ ├── timeline/
│ │ ├── TimelineItemPaymentContent.kt
│ │ ├── TimelineItemContentPaymentFactory.kt
│ │ └── TimelineItemPaymentView.kt
│ └── slash/
│ ├── SlashCommandParser.kt
│ └── SlashCommandHandler.kt
└── test/
└── src/main/kotlin/io/element/android/features/wallet/test/
├── FakeCardanoWalletManager.kt
└── FakeBlockfrostClient.kt
```
---
## 7. Blockers & Gotchas
### Hard Blockers
1. **Account Data API not exposed**
- Impact: No cross-device wallet sync
- Mitigation: Local storage for MVP; SDK extension for V2
- Effort: 2-3 days to extend SDK
2. **Custom event types not recognized**
- Impact: Payment events show as "Unknown event"
- Mitigation: Extend SDK's event parsing or use raw JSON
- Effort: 1-2 days
### Soft Blockers
3. **No existing Cardano library for Android/Kotlin**
- We need: Key derivation (BIP39/CIP1852), transaction building
- Options: cardano-serialization-lib (Rust via JNI), or port to Kotlin
- Effort: 3-5 days for JNI wrapper OR 1-2 weeks for native Kotlin
4. **Blockfrost dependency**
- Need API key management
- Rate limiting concerns
- Effort: 1 day
### Things That Are Easier Than Expected
- **Keystore integration** — Existing infrastructure is excellent
- **Timeline rendering** — Factory pattern makes it trivial
- **Feature module structure** — Well-documented pattern
- **Slash command hook** — Clean insertion point exists
---
## 8. Effort Estimate
### MVP: `/pay` Command End-to-End
| Task | Days | Notes |
|------|------|-------|
| Slash command parser + suggestions | 2 | Extend existing infrastructure |
| Payment flow UI (recipient, amount, confirm) | 3 | Compose screens |
| Cardano key storage (Keystore) | 2 | Extend SecretKeyRepository |
| Cardano transaction builder | 5 | Depends on Rust lib or Kotlin port |
| Blockfrost integration | 1 | Submit tx, query balance |
| Payment card timeline item | 2 | New content type + view |
| Testing & polish | 3 | Unit tests, UI tests, edge cases |
| **Total MVP** | **18 days** | ~3.5 weeks |
### V2: Cross-Device Sync
| Task | Days | Notes |
|------|------|-------|
| Extend SDK for account data | 3 | Rust bindings + Kotlin wrapper |
| Encrypted seed backup | 2 | Store in m.account_data |
| Receive flow + QR | 2 | Address display, sharing |
| **Total V2** | **7 days** | ~1.5 weeks |
### Full Feature Set
| Task | Days | Notes |
|------|------|-------|
| MVP | 18 | As above |
| V2 | 7 | Cross-device |
| Balance widget | 2 | Room-level balance display |
| Transaction history | 3 | Query + display |
| Multi-asset support | 3 | Native tokens |
| **Total Full** | **33 days** | ~6.5 weeks |
---
## 9. Recommendations
1. **Start with local-only MVP** — Don't block on SDK extensions
2. **Use cardano-serialization-lib via JNI** — Don't reinvent wheel
3. **Store wallet per-session** — Each Matrix account has its own wallet
4. **Use Blockfrost for MVP** — Move to own node later if needed
5. **Custom `m.payment.cardano` event** — Proper event type, extend SDK
6. **Fork Element X for initial dev** — Easier than patches
---
## 10. Files Quick Reference
| Purpose | Location |
|---------|----------|
| Slash command suggestions | `features/messages/impl/.../suggestions/SuggestionsProcessor.kt` |
| Message composer | `features/messages/impl/.../messagecomposer/MessageComposerPresenter.kt` |
| Keystore | `libraries/cryptography/impl/.../KeyStoreSecretKeyRepository.kt` |
| Timeline factory | `features/messages/impl/.../factories/event/TimelineItemContentFactory.kt` |
| Timeline content view | `features/messages/impl/.../components/event/TimelineItemEventContentView.kt` |
| Content models | `features/messages/impl/.../model/event/TimelineItem*.kt` |
| Widget system | `features/call/impl/.../utils/DefaultCallWidgetProvider.kt` |
| Encrypted DB | `libraries/encrypted-db/src/.../crypto/EncryptedFile.kt` |
| Session storage | `libraries/session-storage/impl/.../DatabaseSessionStore.kt` |
| Matrix client | `libraries/matrix/impl/.../RustMatrixClient.kt` |
---
*End of Technical Review*

View file

@ -1,3 +0,0 @@
source "https://rubygems.org"
gem 'danger'

661
LICENSE
View file

@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -1,6 +0,0 @@
Licensees holding a valid commercial license with Element may use this
software in accordance with the terms contained in a written agreement
between you and Element.
To purchase a commercial license please contact our sales team at
licensing@element.io

View file

@ -0,0 +1,824 @@
# Matrix Secret Storage (SSSS) Technical Review
**Purpose**: Evaluate Matrix's Secret Storage and Sharing Service (SSSS) for storing a Cardano wallet seed encrypted in a user's Matrix account data, enabling automatic restore on new device verification.
**Goal**: Matrix account = wallet backup. No seed phrase entry, no separate backup.
---
## Table of Contents
1. [SSSS Technical Overview](#1-ssss-technical-overview)
2. [Can You Store Custom Secrets?](#2-can-you-store-custom-secrets)
3. [Account Data Event Format](#3-account-data-event-format)
4. [Secret Sharing on New Device Verification](#4-secret-sharing-on-new-device-verification)
5. [matrix-rust-sdk API Surface](#5-matrix-rust-sdk-api-surface)
6. [Offline Device Recovery](#6-offline-device-recovery)
7. [Encryption Details](#7-encryption-details)
8. [Security Analysis](#8-security-analysis)
9. [Precedents for Non-Matrix Secrets](#9-precedents-for-non-matrix-secrets)
10. [Wallet Storage Flow](#10-wallet-storage-flow)
11. [Wallet Restore Flow](#11-wallet-restore-flow)
12. [Spec Gaps and Limitations](#12-spec-gaps-and-limitations)
13. [Recommendations](#13-recommendations)
---
## 1. SSSS Technical Overview
Matrix Secret Storage (SSSS) is an encrypted key/value store backed by [account data events](https://spec.matrix.org/latest/client-server-api/#client-config) on the homeserver.
### Step-by-Step: How SSSS Works
#### 1.1 Secret Storage Key Generation
Two methods to create a Secret Storage Key:
**Method A: Random Key**
1. Generate 32 random bytes as the secret key material
2. Generate a random key ID (32 alphanumeric characters)
3. Derive a verification key using HKDF-SHA-256:
- Input: 32-byte secret key
- Salt: 32 zero bytes
- Info: empty string (`""`)
- Output: 64 bytes → split into AES key (32 bytes) + MAC key (32 bytes)
4. Encrypt 32 zero bytes with derived key, store IV + MAC in key metadata
**Method B: Passphrase-Based**
1. Generate random 32-character salt
2. Run PBKDF2-HMAC-SHA512:
- Input: passphrase
- Salt: generated salt
- Iterations: 500,000 (default)
- Output: 32 bytes
3. Continue as Method A, but store passphrase info in key metadata
#### 1.2 Key Storage on Server
The Secret Storage Key metadata is uploaded as a global account data event:
```
Event Type: m.secret_storage.key.<key_id>
```
Content structure:
```json
{
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
"iv": "<base64>", // 16 bytes, for MAC verification
"mac": "<base64>", // 32 bytes, for key verification
"passphrase": { // Optional, only if passphrase-derived
"algorithm": "m.pbkdf2",
"salt": "<string>",
"iterations": 500000
}
}
```
#### 1.3 Default Key Designation
A separate event marks which key is default:
```
Event Type: m.secret_storage.default_key
```
Content:
```json
{
"key": "<key_id>"
}
```
#### 1.4 Encrypting a Secret
For each secret with name `<secret_name>`:
1. Derive per-secret encryption keys using HKDF-SHA-256:
- Input: 32-byte storage key
- Salt: 32 zero bytes
- Info: secret name as UTF-8 bytes (e.g., `"m.cross_signing.master"`)
- Output: 64 bytes → AES key (first 32) + MAC key (last 32)
2. Generate random 16-byte IV (with bit 63 cleared for implementation compatibility)
3. Encrypt secret using AES-256-CTR with derived AES key
4. Compute HMAC-SHA-256 over ciphertext using derived MAC key
5. Store as account data event with type = secret name
#### 1.5 Decrypting a Secret
1. Fetch account data event for secret name
2. Look up encrypted data for your key ID
3. Derive per-secret keys using HKDF (same as encryption)
4. Verify MAC before decryption
5. Decrypt ciphertext using AES-256-CTR
---
## 2. Can You Store Custom Secrets?
**Yes, absolutely.** SSSS is designed for arbitrary secrets.
The `SecretName` type in Ruma/matrix-rust-sdk accepts any string:
```rust
let my_secret_name = SecretName::from("com.sulkta.cardano.wallet_seed");
```
Convention for custom secrets:
- Use reverse-domain notation: `com.yourorg.secret_type`
- The secret name becomes the account data event type
- No registration required—just use it
**Well-known secrets defined by Matrix:**
- `m.cross_signing.master`
- `m.cross_signing.self_signing`
- `m.cross_signing.user_signing`
- `m.megolm_backup.v1` (backup recovery key)
**Your custom secret would be:**
- `com.sulkta.cardano.wallet_seed` or similar
---
## 3. Account Data Event Format
### 3.1 Secret Event Type
Each secret is stored at:
```
Event Type: <secret_name>
```
### 3.2 Secret Event Content
```json
{
"encrypted": {
"<key_id>": {
"iv": "<base64>", // 16 bytes
"ciphertext": "<base64>", // encrypted secret
"mac": "<base64>" // 32 bytes HMAC
}
}
}
```
Example for a Cardano wallet seed:
```json
{
"type": "com.sulkta.cardano.wallet_seed",
"content": {
"encrypted": {
"abc123": {
"iv": "gH2iNpiETFhApvW6/FFEJQ==",
"ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=",
"mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE="
}
}
}
}
```
### 3.3 Multiple Key Support
The `encrypted` map can contain entries for multiple storage keys. This allows:
- Key rotation without re-fetching secrets
- Multiple authorized keys (though uncommon)
---
## 4. Secret Sharing on New Device Verification
### 4.1 The Secret Sharing Protocol
When a new device verifies via cross-signing, it can request secrets from existing devices:
**Step 1: New device sends `m.secret.request`**
```json
{
"type": "m.secret.request",
"content": {
"action": "request",
"name": "com.sulkta.cardano.wallet_seed",
"requesting_device_id": "NEWDEVICE",
"request_id": "unique-request-id"
}
}
```
- Sent as **unencrypted** to-device event to all user's devices
**Step 2: Existing device sends `m.secret.send`**
```json
{
"type": "m.secret.send",
"content": {
"request_id": "unique-request-id",
"secret": "<plaintext secret value>"
}
}
```
- Sent as **Olm-encrypted** to-device event
- Only sent to verified devices (cross-signed by owner)
### 4.2 Verification Requirements
Before sharing secrets, the existing device MUST verify:
1. The requesting device is owned by the same user
2. The requesting device is **verified** (cross-signed)
3. The Olm session was established with the expected device
### 4.3 Limitations
**Critical for your use case:**
- Secret sharing requires **at least one other device to be online**
- If no other devices are online, the new device must:
1. Enter the recovery key/passphrase manually
2. Decrypt secrets from account data directly
---
## 5. matrix-rust-sdk API Surface
### 5.1 Opening the Secret Store
```rust
use matrix_sdk::Client;
use ruma::events::secret::request::SecretName;
// Open with recovery key (base58) or passphrase
let secret_store = client
.encryption()
.secret_storage()
.open_secret_store("EsTj 3yST y93F SLpB...") // or passphrase
.await?;
```
### 5.2 Storing a Secret
```rust
let wallet_seed = "your 24 word mnemonic here";
let secret_name = SecretName::from("com.sulkta.cardano.wallet_seed");
secret_store.put_secret(secret_name, wallet_seed).await?;
```
### 5.3 Retrieving a Secret
```rust
let secret_name = SecretName::from("com.sulkta.cardano.wallet_seed");
let wallet_seed = secret_store.get_secret(secret_name).await?;
match wallet_seed {
Some(seed) => println!("Restored wallet seed!"),
None => println!("No wallet seed found in SSSS"),
}
```
### 5.4 Creating a New Secret Store
```rust
// Creates new storage key, uploads to server, re-encrypts existing secrets
let secret_store = client
.encryption()
.secret_storage()
.create_secret_store()
.await?;
// Optionally with passphrase
let secret_store = client
.encryption()
.secret_storage()
.create_secret_store()
.with_passphrase("user's chosen passphrase")
.await?;
// Get the recovery key for backup
let recovery_key = secret_store.secret_storage_key();
println!("Save this recovery key: {}", recovery_key);
```
### 5.5 Recovery on New Device
```rust
// User enters recovery key or passphrase
let recovery = client.encryption().recovery();
recovery.recover("EsTj 3yST y93F SLpB...").await?;
// Now secret store is available
let secret_store = client
.encryption()
.secret_storage()
.open_secret_store("EsTj 3yST y93F SLpB...")
.await?;
// Retrieve wallet seed
let wallet_seed = secret_store
.get_secret(SecretName::from("com.sulkta.cardano.wallet_seed"))
.await?;
```
### 5.6 Importing All Secrets (Built-in Recovery Flow)
```rust
// This imports cross-signing keys + backup key, verifies device
secret_store.import_secrets().await?;
// For custom secrets, call get_secret() separately
let wallet_seed = secret_store.get_secret(my_secret_name).await?;
```
---
## 6. Offline Device Recovery
**Question**: What if no other devices are online when verifying a new device?
### 6.1 SSSS Retrieval Still Works
Secrets in SSSS are stored as **account data events on the homeserver**. They can be retrieved anytime:
1. User enters recovery key/passphrase
2. Client fetches `m.secret_storage.key.<key_id>` event
3. Client derives secret storage key from passphrase (or decodes base58)
4. Client fetches secret's account data event
5. Client decrypts locally
**No other devices needed for SSSS retrieval.**
### 6.2 Limitations
What **does** require other devices online:
- Interactive verification (emoji comparison, QR scan)
- Secret *sharing* via `m.secret.request`/`m.secret.send`
What **doesn't** require other devices:
- Recovery via passphrase/recovery key entry
- Decrypting secrets from account data
### 6.3 Implication for Wallet Recovery
Your wallet seed recovery flow:
1. User logs into new device
2. User enters Matrix recovery key OR passphrase
3. Client opens secret store, retrieves wallet seed
4. Wallet is restored
**This works even if no other devices are online.**
---
## 7. Encryption Details
### 7.1 Algorithm: `m.secret_storage.v1.aes-hmac-sha2`
| Component | Algorithm | Parameters |
|-----------|-----------|------------|
| Key Derivation (passphrase) | PBKDF2-HMAC-SHA512 | 500,000 iterations, 32-byte output |
| Per-Secret Key Derivation | HKDF-SHA256 | 32 zero-byte salt, secret name as info, 64-byte output |
| Encryption | AES-256-CTR | 16-byte IV, bit 63 cleared |
| Authentication | HMAC-SHA256 | 32-byte MAC |
### 7.2 IV Handling
```rust
// Generate 16 random bytes
let mut iv = [0u8; 16];
rng.fill_bytes(&mut iv);
// Clear bit 63 for cross-implementation compatibility
// (Some implementations have issues with high counter values)
let mut iv = u128::from_be_bytes(iv);
iv &= !(1 << 63);
iv.to_be_bytes()
```
### 7.3 Key Derivation Flow
```
Passphrase → PBKDF2 → 32-byte Master Key
HKDF(master_key, salt=zeros, info=secret_name)
64 bytes output
/ \
AES Key (32) MAC Key (32)
```
### 7.4 Encrypt-then-MAC
1. Encrypt plaintext with AES-CTR → ciphertext
2. HMAC(ciphertext) → MAC
3. Store: {iv, ciphertext, mac}
On decrypt:
1. Verify MAC first (constant-time comparison)
2. Only if MAC valid, decrypt ciphertext
---
## 8. Security Analysis
### 8.1 Is Storing a Cardano Wallet Seed Safe?
**Answer: Yes, with caveats.**
#### What Makes It Safe
1. **Strong Encryption**: AES-256-CTR + HMAC-SHA256 is a well-established authenticated encryption scheme
2. **Good Key Derivation**:
- PBKDF2 with 500K iterations provides solid protection against offline brute-force
- HKDF provides proper domain separation per secret
3. **Server Never Sees Plaintext**: Secrets are encrypted client-side before upload
4. **MAC Verification**: Ciphertext integrity is verified before decryption
5. **Zeroization**: matrix-rust-sdk zeroizes key material after use
#### What Must Be True for Safety
1. **Strong Recovery Key/Passphrase**:
- Base58 recovery key: 256 bits of entropy (excellent)
- Passphrase: security depends entirely on passphrase strength
- **Recommendation**: Always use the generated recovery key, not a passphrase
2. **Homeserver Trust Model**:
- Homeserver sees encrypted blobs only
- Homeserver cannot decrypt without recovery key
- But homeserver could:
- Delete your secrets (availability attack)
- Serve old/modified encrypted data (but MAC will fail)
- **For high-value wallets**: Consider self-hosted homeserver
3. **Client Security**:
- Recovery key must never be logged, displayed insecurely, or transmitted
- Device must be free of malware at secret creation time
4. **No Key Escrow**:
- Matrix recovery key is the ONLY way to decrypt
- Lose recovery key = lose access to encrypted secrets
- **Critical**: User must securely back up recovery key
### 8.2 Threat Model
| Threat | Protection | Notes |
|--------|------------|-------|
| Homeserver compromise | AES-256-CTR + HMAC | Attacker needs recovery key |
| Network eavesdropping | TLS + encrypted at rest | Double protection |
| Weak passphrase | PBKDF2 500K iterations | Still vulnerable to offline attack with weak passphrase |
| Malicious client | None | Must trust client code |
| Lost recovery key | None | Permanent loss of secrets |
### 8.3 Comparison to Hardware Wallet
| Factor | SSSS | Hardware Wallet |
|--------|------|-----------------|
| Key never leaves device | ❌ (stored encrypted on server) | ✅ |
| Survives device loss | ✅ | ❌ (unless backed up) |
| Requires extra hardware | ❌ | ✅ |
| Single point of failure | Recovery key | Physical device |
| Brute-force resistant | Depends on passphrase | Very high |
### 8.4 Security Recommendations
1. **Use generated recovery key, not passphrase** for wallet seeds
2. **Store recovery key offline** (paper, metal backup, safety deposit)
3. **Never store recovery key digitally** in cloud services
4. **Self-host Matrix** if storing significant value
5. **Consider additional encryption layer** (encrypt wallet seed before storing in SSSS)
6. **Inform users clearly**: Recovery key loss = permanent wallet loss
---
## 9. Precedents for Non-Matrix Secrets
### 9.1 Existing Non-E2EE Secrets in SSSS
Matrix itself stores non-E2EE-specific secrets:
- `m.megolm_backup.v1` - Key backup recovery key
This demonstrates SSSS is designed for arbitrary cryptographic material.
### 9.2 No Known External Precedents
Research didn't find documented cases of:
- Cryptocurrency wallets using SSSS
- Third-party applications storing secrets in Matrix
**You would be pioneering this use case.**
### 9.3 Design Considerations from Spec Authors
From Matrix spec rationale:
> "The secret storage mechanism provides a way to store arbitrary secrets on the homeserver encrypted by a key that only the user has access to."
The design explicitly anticipates custom secrets.
---
## 10. Wallet Storage Flow
### Step-by-Step: Storing Cardano Wallet Seed
```
1. User creates new wallet in Element X Ada
└── Generate 24-word mnemonic (BIP-39)
2. Check if SSSS is set up
├── If yes: Open existing secret store
└── If no: Prompt user to set up recovery
└── Create new secret storage key
└── Save recovery key for user
3. Encrypt and store wallet seed
└── secret_store.put_secret("com.sulkta.cardano.wallet_seed", mnemonic).await?
4. Account data uploaded:
Event: com.sulkta.cardano.wallet_seed
Content: {
"encrypted": {
"<key_id>": {
"iv": "...",
"ciphertext": "...",
"mac": "..."
}
}
}
5. User sees: "Wallet backed up to your Matrix account"
```
### Code Example
```rust
use matrix_sdk::Client;
use ruma::events::secret::request::SecretName;
const WALLET_SECRET_NAME: &str = "com.sulkta.cardano.wallet_seed";
pub async fn store_wallet_seed(
client: &Client,
mnemonic: &str,
) -> Result<(), Error> {
let secret_storage = client.encryption().secret_storage();
// Check if recovery is set up
if !secret_storage.is_enabled().await? {
return Err(Error::RecoveryNotSetUp);
}
// User must enter recovery key to unlock secret store
let recovery_key = prompt_user_for_recovery_key();
let secret_store = secret_storage
.open_secret_store(&recovery_key)
.await?;
// Store the wallet seed
let secret_name = SecretName::from(WALLET_SECRET_NAME);
secret_store.put_secret(secret_name, mnemonic).await?;
Ok(())
}
```
---
## 11. Wallet Restore Flow
### Step-by-Step: Restoring on New Device
```
1. User logs into Matrix on new device
└── Standard Matrix login flow
2. Element X Ada detects no local wallet
└── Check for SSSS secret: com.sulkta.cardano.wallet_seed
3. User enters recovery key
└── Open secret store with recovery key
4. Retrieve and decrypt wallet seed
└── secret_store.get_secret("com.sulkta.cardano.wallet_seed")
5. Restore Cardano wallet from mnemonic
└── BIP-39 → BIP-32 derivation
6. User sees: "Wallet restored from your Matrix account"
```
### Code Example
```rust
pub async fn restore_wallet_seed(
client: &Client,
recovery_key: &str,
) -> Result<Option<String>, Error> {
let secret_storage = client.encryption().secret_storage();
// Open secret store with user's recovery key
let secret_store = secret_storage
.open_secret_store(recovery_key)
.await?;
// Retrieve wallet seed
let secret_name = SecretName::from(WALLET_SECRET_NAME);
let wallet_seed = secret_store.get_secret(secret_name).await?;
Ok(wallet_seed)
}
pub async fn auto_restore_on_verification(client: &Client) {
// After cross-signing verification succeeds, try to restore
let recovery = client.encryption().recovery();
// If recovery is already set up and we have the key...
if recovery.state() == RecoveryState::Enabled {
// Secret sharing: request from other devices
// This only works if other devices are online
// Fallback: prompt user for recovery key
let key = prompt_for_recovery_key();
if let Ok(seed) = restore_wallet_seed(&client, &key).await {
initialize_cardano_wallet(seed);
}
}
}
```
---
## 12. Spec Gaps and Limitations
### 12.1 No Secret Deletion
**Problem**: Account data events cannot be deleted, only overwritten.
**Workaround**: To "delete" a secret, overwrite with empty encrypted content:
```json
{
"encrypted": {}
}
```
**Implication**: Old encrypted versions may persist in homeserver history/backups.
### 12.2 Secret Sharing Not Automatic for Custom Secrets
**Problem**: `import_secrets()` only imports well-known secrets (cross-signing, backup key).
**Workaround**: After recovery, explicitly call `get_secret()` for your custom secret.
### 12.3 No Push Notification for Secret Availability
**Problem**: New device doesn't know if secrets exist until it tries to fetch them.
**Workaround**: Check for secret existence as part of onboarding flow.
### 12.4 Race Conditions on Secret Update
**Problem**: Concurrent updates from multiple devices can overwrite each other.
**Mitigation**: matrix-rust-sdk uses a lock, but only within single client instance.
**For wallets**: Wallet creation is typically one-time; updates are rare.
### 12.5 No Versioning
**Problem**: No built-in version tracking for secrets.
**Workaround**: Include version in secret content:
```json
{
"version": 1,
"mnemonic": "word word word..."
}
```
### 12.6 Secret Size Limits
**Problem**: Account data events have practical size limits (varies by homeserver).
**Wallet seed**: 24 words ≈ 200 bytes. Well under any reasonable limit.
### 12.7 Recovery Key UX
**Problem**: Users must save recovery key separately. Loss = permanent loss.
**Mitigation**:
- Clear user education
- Multiple recovery methods (passphrase + recovery key)
- Recovery key QR code for easier backup
---
## 13. Recommendations
### 13.1 Implementation Recommendations
1. **Use reverse-domain secret name**: `com.sulkta.cardano.wallet_seed`
2. **Store structured data**:
```json
{
"version": 1,
"format": "bip39",
"mnemonic": "word word word...",
"created_at": "2024-01-01T00:00:00Z"
}
```
3. **Always require recovery key entry for wallet operations**:
- Don't cache the secret store key
- Re-prompt for recovery key when accessing wallet seed
4. **Integrate with existing recovery flow**:
- Piggyback on Matrix recovery setup
- Don't create separate "wallet backup" flow
5. **Handle missing SSSS gracefully**:
- If user hasn't set up recovery, prompt them to do so
- Allow manual seed export as fallback
### 13.2 Security Recommendations
1. **Strongly encourage recovery key over passphrase**
2. **Warn users about recovery key loss consequences**
3. **Consider additional application-layer encryption**
4. **Audit matrix-rust-sdk integration thoroughly**
5. **Self-hosting recommendation for high-value wallets**
### 13.3 UX Recommendations
1. **Unified recovery flow**: One recovery key for Matrix + wallet
2. **Clear onboarding**: Explain that Matrix account = wallet backup
3. **Recovery key export**: QR code, downloadable file, printable
4. **Regular recovery key verification prompts**
---
## Appendix A: Event Schema Summary
### m.secret_storage.key.\<key_id\>
```yaml
algorithm: "m.secret_storage.v1.aes-hmac-sha2"
iv: Base64 (16 bytes) # Optional, for key verification
mac: Base64 (32 bytes) # Optional, for key verification
passphrase: # Optional
algorithm: "m.pbkdf2"
salt: String
iterations: Integer
```
### m.secret_storage.default_key
```yaml
key: String # The key_id of the default key
```
### \<secret_name\> (e.g., com.sulkta.cardano.wallet_seed)
```yaml
encrypted:
<key_id>:
iv: Base64 (16 bytes)
ciphertext: Base64
mac: Base64 (32 bytes)
```
---
## Appendix B: Relevant Source Files (matrix-rust-sdk)
| File | Purpose |
|------|---------|
| `crates/matrix-sdk/src/encryption/secret_storage/mod.rs` | High-level SecretStorage API |
| `crates/matrix-sdk/src/encryption/secret_storage/secret_store.rs` | SecretStore: put_secret, get_secret |
| `crates/matrix-sdk/src/encryption/recovery/mod.rs` | Recovery flows |
| `crates/matrix-sdk-crypto/src/secret_storage.rs` | Low-level SecretStorageKey, encryption |
| `crates/matrix-sdk-crypto/src/ciphers.rs` | AES-CTR + HMAC implementation |
| `crates/matrix-sdk-crypto/src/gossiping/mod.rs` | Secret request/send handling |
---
## Conclusion
**Matrix SSSS is technically suitable for storing a Cardano wallet seed.**
Strengths:
- Strong encryption (AES-256-CTR + HMAC-SHA256)
- Clean API for custom secrets
- Works offline (no other devices needed for recovery)
- Unified with Matrix identity/key management
Risks to mitigate:
- Recovery key loss = permanent wallet loss
- Homeserver trust (for availability, not confidentiality)
- User education on security model
Recommended approach:
1. Integrate wallet seed storage into existing Matrix recovery flow
2. Use generated recovery key (not passphrase) for wallet secrets
3. Clear user communication that recovery key is critical
4. Consider additional encryption layer for defense-in-depth

3032
PHASE1-PLAN.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,34 +0,0 @@
# Phase 1 Status — COMPLETE ✅
## Verification Date
2026-03-28
## What Was Verified
- APK: `app-gplay-x86_64-debug.apk` built from `phase1-dev` branch
- Installed on Android emulator `budtmo/docker-android:emulator_14.0` (emulator-5554)
- Signed in as `@testbot-elementx:sulkta.com` via OIDC (MAS at mas.sulkta.com)
- Opened DM room with `@cobb:sulkta.com`
- Typed `/pay` in message composer
## Result
✅ Slash command autocomplete appeared showing:
- Command: `/pay`
- Description: "Send ADA to someone"
## Phase 1 Bar (Option A) — All Conditions Met
- [x] App launches without crash
- [x] `/pay` appears in slash command autocomplete
- [x] Payment screens navigable (wired in DI graph)
- [x] No live testnet transaction required
## Build Info
- Gradle task: `:app:assembleGplayDebug`
- Branch: `phase1-dev`
- Final commit: `ad89eddfea`
- Build image: `mingc/android-build-box:latest` (Java 21)
## Key Fixes Applied
1. Metro DI scope mismatch: CardanoWalletManager removed CardanoClient dep (AppScope vs SessionScope)
2. WalletState constructor: all required fields populated
3. Packaging conflict: moshi-kotlin-codegen/lombok META-INF pickFirst
4. Build flavor: assembleGplayDebug (not fdroid, not plain assembleDebug)

115
README.md
View file

@ -1,114 +1,3 @@
[![Latest build](https://github.com/element-hq/element-x-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/element-hq/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=element-x-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=element-x-android)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-x-android&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-x-android)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-x-android&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-x-android)
[![codecov](https://codecov.io/github/element-hq/element-x-android/branch/develop/graph/badge.svg?token=ecwvia7amV)](https://codecov.io/github/element-hq/element-x-android)
[![Element X Android Matrix room #element-x-android:matrix.org](https://img.shields.io/matrix/element-x-android:matrix.org.svg?label=%23element-x-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-x-android:matrix.org)
[![Localazy](https://img.shields.io/endpoint?url=https%3A%2F%2Fconnect.localazy.com%2Fstatus%2Felement%2Fdata%3Fcontent%3Dall%26title%3Dlocalazy%26logo%3Dtrue)](https://localazy.com/p/element)
# element-x-ada
# Element X Android
Element X Android is the next-generation [Matrix](https://matrix.org/) client provided by [Element](https://element.io/).
Compared to the previous-generation [Element Classic](https://github.com/element-hq/element-android), the application is a total rewrite, using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=io.element.android.x)[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/io.element.android.x)
## Table of contents
<!--- TOC -->
* [Screenshots](#screenshots)
* [Translations](#translations)
* [Rust SDK](#rust-sdk)
* [Status](#status)
* [Minimum SDK version](#minimum-sdk-version)
* [Contributing](#contributing)
* [Build instructions](#build-instructions)
* [Support](#support)
* [Copyright and License](#copyright-and-license)
<!--- END -->
## Screenshots
Here are some screenshots of the application:
<!--
Commands run before taking the screenshots:
adb shell settings put system time_12_24 24
adb shell am broadcast -a com.android.systemui.demo -e command enter
adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1337
adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level 4
adb shell am broadcast -a com.android.systemui.demo -e command network -e wifi show -e level 4
adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false
adb shell am broadcast -a com.android.systemui.demo -e command battery -e plugged false -e level 100
And to exit demo mode:
adb shell am broadcast -a com.android.systemui.demo -e command exit
-->
|<img src="./docs/images-lfs/screen_1_light.png" width="280" />|<img src="./docs/images-lfs/screen_2_light.png" width="280" />|<img src="./docs/images-lfs/screen_3_light.png" width="280" />|<img src="./docs/images-lfs/screen_4_light.png" width="280" />|
|-|-|-|-|
|<img src="./docs/images-lfs/screen_1_dark.png" width="280" />|<img src="./docs/images-lfs/screen_2_dark.png" width="280" />|<img src="./docs/images-lfs/screen_3_dark.png" width="280" />|<img src="./docs/images-lfs/screen_4_dark.png" width="280" />|
## Translations
Element X Android supports many languages. You can help us to translate the app in your language by joining our [Localazy project](https://localazy.com/p/element). You can also help us to improve the existing translations.
Note that for now, we keep control on the French and German translations.
Translations can be checked screen per screen using our tool Element X Android Gallery, available at https://element-hq.github.io/element-x-android/. Note that this page is updated every Tuesday.
More instructions about translating the application can be found at [CONTRIBUTING.md](CONTRIBUTING.md#strings).
## Rust SDK
Element X leverages the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) through an FFI layer that the final client can directly import and use.
We're doing this as a way to share code between platforms and while we've seen promising results it's still in the experimental stage and bound to change.
## Status
This project is actively developed and supported. New users are recommended to use Element X instead of the previous-generation app.
## Minimum SDK version
Element X Android requires a minimum SDK version of 24 (Android 7.0, Nougat). We aim to support devices running Android 7.0 and above, which covers a wide range of devices still in use today.
Element Android Enterprise requires a minimum SDK version of 33 (Android 13, Tiramisu). For Element Enterprise, we support only devices that still receive security updates, which means devices running Android 13 and above. Android does not have a documented support policy, but some information can be found at [https://endoflife.date/android](https://endoflife.date/android).
## Contributing
Want to get actively involved in the project? You're more than welcome! A good way to start is to check the issues that are labelled with the [good first issue](https://github.com/element-hq/element-x-android/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label. Let us know by commenting the issue that you're starting working on it.
But first make sure to read our [contribution guide](CONTRIBUTING.md) first.
You can also come chat with the community in the Matrix [room](https://matrix.to/#/#element-x-android:matrix.org) dedicated to the project.
## Build instructions
Just clone the project and open it in Android Studio. Make sure to select the
`app` configuration when building (as we also have sample apps in the project).
To build against a local copy of the Rust SDK, see the [Developer
onboarding](docs/_developer_onboarding.md#building-the-sdk-locally) instructions.
## Support
When you are experiencing an issue on Element X Android, please first search in [GitHub issues](https://github.com/element-hq/element-x-android/issues)
and then in [#element-x-android:matrix.org](https://matrix.to/#/#element-x-android:matrix.org).
If after your research you still have a question, ask at [#element-x-android:matrix.org](https://matrix.to/#/#element-x-android:matrix.org). Otherwise feel free to create a GitHub issue if you encounter a bug or a crash, by explaining clearly in detail what happened. You can also perform bug reporting from the application settings. This is especially recommended when you encounter a crash.
## Copyright and License
Copyright (c) 2025 Element Creations Ltd.
Copyright (c) 2022 - 2025 New Vector Ltd.
This software is dual licensed by Element Creations Ltd (Element). It can be used either:
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
Element X fork with native Cardano wallet

78
SYNC.md
View file

@ -1,78 +0,0 @@
# Repo topology + upstream sync procedure
This repo is a fork of [`element-hq/element-x-android`](https://github.com/element-hq/element-x-android)
with a native Cardano wallet module added. The history is structured so that
staying current with upstream — and one day proposing our additions back —
stays possible.
## Branches
| Branch | Role |
|--------|------|
| `main` | Tracks the upstream commit we are currently based on. Fast-forwarded to `upstream/develop` when we deliberately pull in changes. Nothing coop-specific lives here. |
| `wallet` | `main` + all our wallet work. This is what we build APKs from. Linear history on top of `main`; rebased whenever `main` moves. |
| `archive/project-docs` | Frozen snapshot of the planning docs and screenshots that lived on the original orphan `main` branch. Not part of the active graph. |
When we ever want a clean "everything we'd propose upstream" branch, we cherry-pick
the wallet commits off `wallet` onto a fresh branch rooted at `main`. Because every
current commit on `wallet` is wallet-module work, that split is simple.
## Remotes
`origin` → this Gitea repo (LAN, via the Rackham SSH tunnel when working remotely).
Add upstream on any local clone:
```bash
git remote add upstream https://github.com/element-hq/element-x-android.git
git fetch upstream
```
## Sync with upstream
When you want to pick up the latest from `element-hq/element-x-android`:
```bash
# 1. Get the latest from upstream
git fetch upstream
# 2. Fast-forward main to upstream/develop
git checkout main
git merge --ff-only upstream/develop
git push origin main
# 3. Rebase wallet onto the new main
git checkout wallet
git rebase main
# → resolve conflicts, one commit at a time
# → conflict surface is small but real: our integration touches
# libraries/matrix/{api,impl}, libraries/textcomposer/impl,
# libraries/eventformatter/impl, libraries/mediaviewer/impl
# 4. Build + test the APK before force-pushing
./gradlew :app:assembleFdroidDebug # or mainnet variant
# 5. Push the rebased wallet branch (force-with-lease, not plain force)
git push --force-with-lease origin wallet
```
If the rebase gets ugly, abort and try merging instead:
```bash
git rebase --abort
git merge upstream/develop
# resolves in one shot, one merge commit, less clean history
```
## Why not a Gitea mirror?
Gitea only lets you configure a pull-mirror at repo-creation time, and mirroring
a whole repo also means we can't commit to it. We want to keep our own commits,
so upstream stays as a git remote you fetch from manually.
## License
Upstream is **AGPL-3.0**. Every binary we hand out must be accompanied by the
corresponding source under the same license. Keeping this Gitea repo accessible
to recipients of the APK satisfies that. Don't ship binaries without also making
the source reachable.

View file

@ -1 +0,0 @@
/build

View file

@ -1,10 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.jvm-library")
}

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.annotations
import kotlin.reflect.KClass
/**
* Adds Node to the specified component graph.
* Equivalent to the following declaration:
*
* @BindingContainer
* @ContributesTo(Scope::class)
* abstract class YourNodeModule {
* @Binds
* @IntoMap
* @NodeKey(YourNode::class)
* abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
*}
*/
@Target(AnnotationTarget.CLASS)
annotation class ContributesNode(
val scope: KClass<*>,
)

View file

@ -1,362 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:Suppress("UnstableApiUsage")
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.android.build.gradle.tasks.GenerateBuildConfig
import com.google.firebase.appdistribution.gradle.firebaseAppDistribution
import config.BuildTimeConfig
import extension.AssetCopyTask
import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource
import extension.allEnterpriseImpl
import extension.allFeaturesImpl
import extension.allLibrariesImpl
import extension.allServicesImpl
import extension.buildConfigFieldStr
import extension.locales
import extension.setupDependencyInjection
import extension.testCommonDependencies
import java.util.Locale
plugins {
id("io.element.android-compose-application")
alias(libs.plugins.kotlin.android)
// When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId)
id("kotlin-parcelize")
alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization)
// To be able to update the firebase.xml files, uncomment and build the project
// alias(libs.plugins.gms.google.services)
}
android {
namespace = "io.element.android.x"
defaultConfig {
applicationId = BuildTimeConfig.APPLICATION_ID
targetSdk = Versions.TARGET_SDK
versionCode = Versions.VERSION_CODE
versionName = Versions.VERSION_NAME
// Keep abiFilter for the universalApk
ndk {
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
}
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
splits {
// Configures multiple APKs based on ABI.
abi {
val buildingAppBundle = gradle.startParameter.taskNames.any { it.contains("bundle") }
// Enables building multiple APKs per ABI. This should be disabled when building an AAB.
isEnable = !buildingAppBundle
// By default all ABIs are included, so use reset() and include to specify that we only
// want APKs for armeabi-v7a, x86, arm64-v8a and x86_64.
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
if (!buildingAppBundle) {
// Specifies a list of ABIs that Gradle should create APKs for.
include("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
// Generate a universal APK that includes all ABIs, so user who installs from CI tool can use this one by default.
isUniversalApk = true
}
}
}
androidResources {
localeFilters += locales
}
}
signingConfigs {
getByName("debug") {
keyAlias = "androiddebugkey"
keyPassword = "android"
storeFile = file("./signature/debug.keystore")
storePassword = "android"
}
register("nightly") {
keyAlias = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYID")
?: project.property("signing.element.nightly.keyId") as? String?
keyPassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD")
?: project.property("signing.element.nightly.keyPassword") as? String?
storeFile = file("./signature/nightly.keystore")
storePassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD")
?: project.property("signing.element.nightly.storePassword") as? String?
}
}
val baseAppName = BuildTimeConfig.APPLICATION_NAME
val buildType = if (isEnterpriseBuild) "Enterprise" else "FOSS"
logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]")
buildTypes {
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
getByName("debug") {
resValue("string", "app_name", "$baseAppName dbg")
resValue(
"string",
"login_redirect_scheme",
"$oidcRedirectSchemeBase.debug",
)
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
getByName("release") {
resValue("string", "app_name", baseAppName)
resValue(
"string",
"login_redirect_scheme",
oidcRedirectSchemeBase,
)
signingConfig = signingConfigs.getByName("debug")
optimization {
enable = true
keepRules {
files.add(File(projectDir, "common-proguard-rules.pro"))
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardFile = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
File(projectDir, "enterprise-proguard-rules.pro")
} else {
// These default rules prevent the OSS app from being obfuscated
File(projectDir, "default-proguard-rules.pro")
}
if (flavorProguardFile.exists()) {
files.add(flavorProguardFile)
} else {
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
}
}
}
}
register("nightly") {
val release = getByName("release")
initWith(release)
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "$baseAppName nightly")
resValue(
"string",
"login_redirect_scheme",
"$oidcRedirectSchemeBase.nightly",
)
matchingFallbacks += listOf("release")
signingConfig = signingConfigs.getByName("nightly")
firebaseAppDistribution {
artifactType = "APK"
// We upload the universal APK to fix this error:
// "App Distribution found more than 1 output file for this variant.
// Please contact firebase-support@google.com for help using APK splits with App Distribution."
artifactPath = "$rootDir/app/build/outputs/apk/gplay/nightly/app-gplay-universal-nightly.apk"
// artifactType = "AAB"
// artifactPath = "$rootDir/app/build/outputs/bundle/nightly/app-nightly.aab"
releaseNotesFile = "tools/release/ReleaseNotesNightly.md"
groups = if (isEnterpriseBuild) {
"enterprise-testers"
} else {
"external-testers"
}
// This should not be required, but if I do not add the appId, I get this error:
// "App Distribution halted because it had a problem uploading the APK: [404] Requested entity was not found."
appId = if (isEnterpriseBuild) {
"1:912726360885:android:3f7e1fe644d99d5a00427c"
} else {
"1:912726360885:android:e17435e0beb0303000427c"
}
}
}
}
buildFeatures {
buildConfig = true
}
flavorDimensions += "store"
productFlavors {
create("gplay") {
dimension = "store"
isDefault = true
buildConfigFieldStr("SHORT_FLAVOR_DESCRIPTION", "G")
buildConfigFieldStr("FLAVOR_DESCRIPTION", "GooglePlay")
}
create("fdroid") {
dimension = "store"
buildConfigFieldStr("SHORT_FLAVOR_DESCRIPTION", "F")
buildConfigFieldStr("FLAVOR_DESCRIPTION", "FDroid")
}
}
packaging {
resources.pickFirsts += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
"META-INF/gradle/incremental.annotation.processors",
)
jniLibs {
useLegacyPackaging = project.findProperty("useLegacyPackaging")?.toString()?.toBoolean()
}
}
}
androidComponents {
// map for the version codes last digit
// x86 must have greater values than arm
// 64 bits have greater value than 32 bits
val abiVersionCodes = mapOf(
"armeabi-v7a" to 1,
"arm64-v8a" to 2,
"x86" to 3,
"x86_64" to 4,
)
onVariants { variant ->
// Assigns a different version code for each output APK
// other than the universal APK.
variant.outputs.forEach { output ->
val name = output.filters.find { it.filterType == ABI }?.identifier
// Stores the value of abiCodes that is associated with the ABI for this variant.
val abiCode = abiVersionCodes[name] ?: 0
// Assigns the new version code to output.versionCode, which changes the version code
// for only the output APK, not for the variant itself.
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
}
}
val reportingExtension: ReportingExtension = project.extensions.getByType(ReportingExtension::class.java)
configureLicensesTasks(reportingExtension)
}
setupDependencyInjection()
dependencies {
allLibrariesImpl()
allServicesImpl()
if (isEnterpriseBuild) {
allEnterpriseImpl(project)
implementation(projects.appicon.enterprise)
} else {
implementation(projects.features.enterprise.implFoss)
implementation(projects.appicon.element)
}
allFeaturesImpl(project)
implementation(projects.features.migration.api)
implementation(projects.appnav)
implementation(projects.appconfig)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.compose)
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
"gplayImplementation"(projects.libraries.pushproviders.firebase)
}
if (ModulesConfig.pushProvidersConfig.includeUnifiedPush) {
implementation(projects.libraries.pushproviders.unifiedpush)
}
implementation(libs.appyx.core)
implementation(libs.androidx.splash)
implementation(libs.androidx.core)
implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.startup)
implementation(libs.androidx.preference)
implementation(libs.coil)
implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp.logging)
implementation(libs.serialization.json)
implementation(libs.matrix.emojibase.bindings)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test)
}
tasks.withType<GenerateBuildConfig>().configureEach {
outputs.upToDateWhen { false }
val gitRevision = providers.of(GitRevisionValueSource::class.java) {}.get()
val gitBranchName = providers.of(GitBranchNameValueSource::class.java) {}.get()
android.defaultConfig.buildConfigFieldStr("GIT_REVISION", gitRevision)
android.defaultConfig.buildConfigFieldStr("GIT_BRANCH_NAME", gitBranchName)
}
licensee {
allow("Apache-2.0")
allow("MIT")
allow("BSD-2-Clause")
allow("BSD-3-Clause")
allow("EPL-1.0")
allowUrl("https://opensource.org/license/bsd-3-clause")
allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/")
allowUrl("https://jsoup.org/license")
allowUrl("https://asm.ow2.io/license.html")
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE")
allowUrl("https://opensource.org/licenses/mit-license.php")
allowUrl("https://github.com/javaee/javax.annotation/blob/master/LICENSE")
allowUrl("https://www.bouncycastle.org/licence.html")
allowUrl("https://projectlombok.org/LICENSE")
allow("CC0-1.0")
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android")
}
fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
androidComponents {
onVariants { variant ->
val capitalizedVariantName = variant.name.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
val artifactsFile = reportingExtension.baseDirectory.file("licensee/android$capitalizedVariantName/artifacts.json")
val copyArtifactsTask =
project.tasks.register<AssetCopyTask>("copy${capitalizedVariantName}LicenseeReportToAssets") {
inputFile.set(artifactsFile)
targetFileName.set("licensee-artifacts.json")
}
variant.sources.assets?.addGeneratedSourceDirectory(
copyArtifactsTask,
AssetCopyTask::outputDirectory,
)
copyArtifactsTask.dependsOn("licenseeAndroid$capitalizedVariantName")
}
}
}
configurations.all {
resolutionStrategy {
dependencySubstitution {
val tink = libs.google.tink.get()
substitute(module("com.google.crypto.tink:tink")).using(module("${tink.group}:${tink.name}:${tink.version}"))
}
}
}

View file

@ -1,79 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Increase optimizations passes to 3
-optimizationpasses 3
# JNA
-dontwarn java.awt.*
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }
# TagSoup, coming from the RTE library
-keep class org.ccil.cowan.tagsoup.** { *; }
# kotlinx.serialization
# Kotlin serialization looks up the generated serializer classes through a function on companion
# objects. The companions are looked up reflectively so we need to explicitly keep these functions.
-keepclasseswithmembers class **.*$Companion {
kotlinx.serialization.KSerializer serializer(...);
}
# If a companion has the serializer function, keep the companion field on the original type so that
# the reflective lookup succeeds.
-if class **.*$Companion {
kotlinx.serialization.KSerializer serializer(...);
}
-keepclassmembers class <1>.<2> {
<1>.<2>$Companion Companion;
}
# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
# Taken from https://raw.githubusercontent.com/square/okhttp/master/okhttp/src/jvmMain/resources/META-INF/proguard/okhttp3.pro
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
# Needed for Posthog
-keepclassmembers class android.view.JavaViewSpy {
static int windowAttachCount(android.view.View);
}
# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
-keep class android.media.metrics.LogSessionId { *; }
-keep class android.media.metrics.** { *; }
# Keep Media3 classes that use reflection (https://github.com/androidx/media/issues/2535)
-keep class androidx.media3.** { *; }
-dontwarn android.media.metrics.**
# New rules after AGP 8.13.1 upgrade
-dontwarn androidx.window.extensions.WindowExtensions
-dontwarn androidx.window.extensions.WindowExtensionsProvider
-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation
-dontwarn androidx.window.extensions.layout.DisplayFeature
-dontwarn androidx.window.extensions.layout.FoldingFeature
-dontwarn androidx.window.extensions.layout.WindowLayoutComponent
-dontwarn androidx.window.extensions.layout.WindowLayoutInfo
-dontwarn androidx.window.sidecar.SidecarDeviceState
-dontwarn androidx.window.sidecar.SidecarDisplayFeature
-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback
-dontwarn androidx.window.sidecar.SidecarInterface
-dontwarn androidx.window.sidecar.SidecarProvider
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
# Also needed after AGP 8.13.1 upgrade, it seems like proguard is now more aggressive on removing unused code
-keep,allowoptimization,allowshrinking class org.matrix.rustcomponents.sdk.** { *;}
-keep,allowoptimization,allowshrinking class uniffi.** { *;}
-keep,allowoptimization,allowshrinking class io.element.android.x.di.** { *; }
# Keep Metro classes
-keep,allowoptimization,allowshrinking class dev.zacsweers.metro.** { *; }
# Rustls Platform Verifier
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }

View file

@ -1,2 +0,0 @@
# Don't obfuscate anything for non-enterprise builds
-dontobfuscate

Binary file not shown.

Binary file not shown.

View file

@ -1,174 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- To be able to install APK from the application -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".ElementXApplication"
android:allowBackup="false"
android:appCategory="social"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ElementX"
tools:targetApi="33">
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name='androidx.lifecycle.ProcessLifecycleInitializer'
android:value='androidx.startup' />
<!-- Remove to use Application workManagerConfiguration -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<!--
Using launchMode singleTask to avoid multiple instances of the Activity
when the app is already open. This is important for incoming share (see
https://github.com/element-hq/element-x-android/issues/4074) and for opening
the application from a mobile.element.io link.
-->
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Handle deep-link for notification ./tools/adb/deeplink.sh -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="open"
android:scheme="elementx" />
</intent-filter>
<!--
Oidc redirection
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/login_redirect_scheme" />
</intent-filter>
<!--
Element web links
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. -->
<!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json -->
<data android:host="app.element.io" />
<!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json -->
<data android:host="develop.element.io" />
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
<data android:host="staging.element.io" />
</intent-filter>
<!--
Element mobile links
Example: https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Matching asset file: https://mobile.element.io/.well-known/assetlinks.json -->
<data android:host="mobile.element.io" />
<data android:path="/element/" />
</intent-filter>
<!--
matrix: links
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="matrix" />
</intent-filter>
<!--
links from matrix.to website
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="element" />
<data android:host="user" />
<data android:host="room" />
</intent-filter>
<!-- Incoming share simple -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
<!-- Incoming share multiple -->
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_providers" />
</provider>
</application>
</manifest>

View file

@ -1,47 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x
import android.app.Application
import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.startup.AppInitializer
import androidx.work.Configuration
import dev.zacsweers.metro.createGraphFactory
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.x.di.AppGraph
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CacheCleanerInitializer
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.PlatformInitializer
class ElementXApplication : Application(), DependencyInjectionGraphOwner, Configuration.Provider {
override val graph: AppGraph = createGraphFactory<AppGraph.Factory>().create(this)
override val workManagerConfiguration: Configuration = Configuration.Builder()
.setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
.build()
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate() {
super.onCreate()
AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java)
initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java)
}
logApplicationInfo(this)
// Disable the strict offset check for anchored draggable components, as it can cause issues with bottom sheets.
// Remove once https://issuetracker.google.com/issues/477038695 is fixed.
isAnchoredDraggableComponentsStrictOffsetCheckEnabled = false
}
}

View file

@ -1,172 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.libraries.architecture.appyx.DebugNavStateNodeHost
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler
import kotlinx.coroutines.launch
import timber.log.Timber
private val loggerTag = LoggerTag("MainActivity")
class MainActivity : NodeActivity() {
private lateinit var mainNode: MainNode
private lateinit var appBindings: AppBindings
override fun onCreate(savedInstanceState: Bundle?) {
Timber.tag(loggerTag.value).d("onCreate, with savedInstanceState: ${savedInstanceState != null}")
installSplashScreen()
super.onCreate(savedInstanceState)
appBindings = bindings()
setupLockManagement(appBindings.lockScreenService(), appBindings.lockScreenEntryPoint())
enableEdgeToEdge()
setContent {
MainContent(appBindings)
}
}
@Composable
private fun MainContent(appBindings: AppBindings) {
val migrationState = appBindings.migrationEntryPoint().present()
val colors by remember {
appBindings.enterpriseService().semanticColorsFlow(sessionId = null)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appBindings.preferencesStore(),
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = appBindings.buildMeta()
) {
CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this),
LocalAnalyticsService provides appBindings.analyticsService(),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault),
) {
if (migrationState.migrationAction.isSuccess()) {
MainNodeHost()
} else {
appBindings.migrationEntryPoint().Render(
state = migrationState,
modifier = Modifier,
)
}
}
}
}
}
@Composable
private fun MainNodeHost() {
// TODO this is a temporary helper to capture the nav state in a more readable format for crash reports
// Revert to `NodeHost` once this is fixed
DebugNavStateNodeHost(integrationPoint = appyxV1IntegrationPoint) {
MainNode(
it,
plugins = listOf(
object : NodeReadyObserver<MainNode> {
override fun init(node: MainNode) {
Timber.tag(loggerTag.value).d("onMainNodeInit")
mainNode = node
mainNode.handleIntent(intent)
}
},
),
context = applicationContext
)
}
}
private fun setupLockManagement(
lockScreenService: LockScreenService,
lockScreenEntryPoint: LockScreenEntryPoint
) {
lockScreenService.handleSecureFlag(this)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Locked) {
startActivity(lockScreenEntryPoint.pinUnlockIntent(this@MainActivity))
}
}
}
}
}
/**
* Called when:
* - the launcher icon is clicked (if the app is already running);
* - a notification is clicked.
* - a deep link have been clicked
* - the app is going to background (<- this is strange)
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Timber.tag(loggerTag.value).d("onNewIntent")
// If the mainNode is not init yet, keep the intent for later.
// It can happen when the activity is killed by the system. The methods are called in this order :
// onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit
if (::mainNode.isInitialized) {
mainNode.handleIntent(intent)
} else {
setIntent(intent)
}
}
override fun onPause() {
super.onPause()
Timber.tag(loggerTag.value).d("onPause")
}
override fun onResume() {
super.onResume()
Timber.tag(loggerTag.value).d("onResume")
}
override fun onDestroy() {
super.onDestroy()
Timber.tag(loggerTag.value).d("onDestroy")
}
}

View file

@ -1,62 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x
import android.content.Context
import android.content.Intent
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class MainNode(
buildContext: BuildContext,
plugins: List<Plugin>,
@ApplicationContext context: Context,
) : ParentNode<MainNode.RootNavTarget>(
navModel = PermanentNavModel(
navTargets = setOf(RootNavTarget),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
),
DependencyInjectionGraphOwner {
override val graph = (context as DependencyInjectionGraphOwner).graph
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
return createNode<RootFlowNode>(buildContext = buildContext)
}
@Composable
override fun View(modifier: Modifier) {
Children(navModel = navModel)
}
fun handleIntent(intent: Intent) {
lifecycleScope.launch {
waitForChildAttached<RootFlowNode>().handleIntent(intent)
}
}
@Parcelize
object RootNavTarget : Parcelable
}

View file

@ -1,54 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.identifiers.SentrySdkDsn
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.platform.InitPlatformService
import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService
@ContributesTo(AppScope::class)
interface AppBindings {
fun snackbarDispatcher(): SnackbarDispatcher
fun tracingService(): TracingService
fun platformService(): InitPlatformService
fun bugReporter(): BugReporter
fun lockScreenService(): LockScreenService
fun preferencesStore(): AppPreferencesStore
fun migrationEntryPoint(): MigrationEntryPoint
fun lockScreenEntryPoint(): LockScreenEntryPoint
fun analyticsService(): AnalyticsService
fun enterpriseService(): EnterpriseService
fun featureFlagService(): FeatureFlagService
fun buildMeta(): BuildMeta
fun sentrySdkDsn(): SentrySdkDsn?
}

View file

@ -1,37 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import android.content.Context
import androidx.work.ListenableWorker
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Multibinds
import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import kotlin.reflect.KClass
@DependencyGraph(AppScope::class)
interface AppGraph : NodeFactoriesBindings {
val sessionGraphFactory: SessionGraph.Factory
@Multibinds
val workerProviders:
Map<KClass<out ListenableWorker>, MetroWorkerFactory.WorkerInstanceFactory<*>>
@DependencyGraph.Factory
interface Factory {
fun create(
@ApplicationContext @Provides
context: Context
): AppGraph
}
}

View file

@ -1,124 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import androidx.preference.PreferenceManager
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
import io.element.android.x.BuildConfig
import io.element.android.x.R
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
import java.io.File
@BindingContainer
@ContributesTo(AppScope::class)
object AppModule {
@Provides
@BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File {
return File(context.filesDir, "sessions")
}
@Provides
@CacheDirectory
fun providesCacheDirectory(@ApplicationContext context: Context): File {
return context.cacheDir
}
@Provides
fun providesResources(@ApplicationContext context: Context): Resources {
return context.resources
}
@Provides
@AppCoroutineScope
@SingleIn(AppScope::class)
fun providesAppCoroutineScope(): CoroutineScope {
return MainScope() + CoroutineName("ElementX Scope")
}
@Provides
@SingleIn(AppScope::class)
fun providesBuildType(): BuildType {
return BuildType.valueOf(BuildConfig.BUILD_TYPE.uppercase())
}
@Provides
@SingleIn(AppScope::class)
fun providesBuildMeta(
@ApplicationContext context: Context,
buildType: BuildType,
enterpriseService: EnterpriseService,
): BuildMeta {
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
return BuildMeta(
isDebuggable = BuildConfig.DEBUG,
buildType = buildType,
applicationName = applicationName,
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
applicationId = BuildConfig.APPLICATION_ID,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME,
versionCode = context.getVersionCodeFromManifest(),
gitRevision = BuildConfig.GIT_REVISION,
gitBranchName = BuildConfig.GIT_BRANCH_NAME,
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION,
)
}
@Provides
@SingleIn(AppScope::class)
fun providesSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
@Provides
@SingleIn(AppScope::class)
fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers.Default
}
@Provides
@SingleIn(AppScope::class)
fun provideSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher()
}
@Provides
@SingleIn(AppScope::class)
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
return DefaultEmojibaseProvider(context)
}
}

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import dev.zacsweers.metro.ContributesBinding
import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
@ContributesBinding(SessionScope::class)
class DefaultRoomGraphFactory(
private val sessionGraph: SessionGraph,
) : RoomGraphFactory {
override fun create(room: JoinedRoom): Any {
return sessionGraph.roomGraphFactory
.create(room, room)
}
}

View file

@ -1,23 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.appnav.di.SessionGraphFactory
import io.element.android.libraries.matrix.api.MatrixClient
@ContributesBinding(AppScope::class)
class DefaultSessionGraphFactory(
private val appGraph: AppGraph
) : SessionGraphFactory {
override fun create(client: MatrixClient): Any {
return appGraph.sessionGraphFactory.create(client)
}
}

View file

@ -1,28 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import dev.zacsweers.metro.GraphExtension
import dev.zacsweers.metro.Provides
import io.element.android.appnav.di.TimelineBindings
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
@GraphExtension(RoomScope::class)
interface RoomGraph : NodeFactoriesBindings, TimelineBindings {
@GraphExtension.Factory
interface Factory {
fun create(
@Provides joinedRoom: JoinedRoom,
@Provides baseRoom: BaseRoom
): RoomGraph
}
}

View file

@ -1,25 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import dev.zacsweers.metro.GraphExtension
import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
@GraphExtension(SessionScope::class)
interface SessionGraph : NodeFactoriesBindings {
val roomGraphFactory: RoomGraph.Factory
@GraphExtension.Factory
interface Factory {
fun create(@Provides matrixClient: MatrixClient): SessionGraph
}
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.info
import android.content.Context
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.x.BuildConfig
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun logApplicationInfo(context: Context) {
val appVersion = buildString {
append(BuildConfig.VERSION_NAME)
append(" (")
append(context.getVersionCodeFromManifest())
append(") - ")
append(BuildConfig.BUILD_TYPE)
append(" / ")
append(BuildConfig.FLAVOR)
}
// TODO Get SDK version somehow
val sdkVersion = "SDK VERSION (TODO)"
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------")
Timber.d(" Application version: $appVersion")
Timber.d(" Git SHA: ${BuildConfig.GIT_REVISION}")
Timber.d(" SDK version: $sdkVersion")
Timber.d(" Local time: $date")
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------\n\n\n\n")
}

View file

@ -1,22 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.initializer
import android.content.Context
import androidx.startup.Initializer
import io.element.android.features.cachecleaner.impl.CacheCleanerBindings
import io.element.android.libraries.architecture.bindings
class CacheCleanerInitializer : Initializer<Unit> {
override fun create(context: Context) {
context.bindings<CacheCleanerBindings>().cacheCleaner().clearCache()
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

View file

@ -1,25 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.initializer
import android.content.Context
import androidx.startup.Initializer
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler
import io.element.android.features.rageshake.impl.di.RageshakeBindings
import io.element.android.libraries.architecture.bindings
class CrashInitializer : Initializer<Unit> {
override fun create(context: Context) {
VectorUncaughtExceptionHandler(
context.bindings<RageshakeBindings>().preferencesCrashDataStore(),
).activate()
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

View file

@ -1,50 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.initializer
import android.content.Context
import android.system.Os
import androidx.startup.Initializer
import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.x.di.AppBindings
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import timber.log.Timber
private const val ELEMENT_X_TARGET = "elementx"
class PlatformInitializer : Initializer<Unit> {
override fun create(context: Context) {
val appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService()
val platformService = appBindings.platformService()
val bugReporter = appBindings.bugReporter()
Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET))
val preferencesStore = appBindings.preferencesStore()
val featureFlagService = appBindings.featureFlagService()
val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() }
val tracingConfiguration = TracingConfiguration(
writesToLogcat = runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PrintLogsToLogcat) },
writesToFilesConfiguration = bugReporter.createWriteToFilesConfiguration(),
logLevel = logLevel,
extraTargets = listOf(ELEMENT_X_TARGET),
traceLogPacks = runBlocking { preferencesStore.getTracingLogPacksFlow().first() },
sdkSentryDsn = appBindings.sentrySdkDsn()?.value?.takeIf { it.isNotBlank() },
)
bugReporter.setCurrentTracingLogLevel(logLevel.name)
platformService.init(tracingConfiguration)
// Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true)
}
override fun dependencies(): List<Class<out Initializer<*>>> = mutableListOf()
}

View file

@ -1,44 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.intent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.deeplink.api.DeepLinkCreator
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.x.MainActivity
@ContributesBinding(AppScope::class)
class DefaultIntentProvider(
@ApplicationContext private val context: Context,
private val deepLinkCreator: DeepLinkCreator,
) : IntentProvider {
override fun getViewRoomIntent(
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
eventId: EventId?,
extras: Bundle?,
): Intent {
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = deepLinkCreator.create(sessionId, roomId, threadId, eventId).toUri()
extras?.let(::putExtras)
}
}
}

View file

@ -1,19 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.intent
import android.app.Activity
import androidx.compose.ui.platform.UriHandler
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
class SafeUriHandler(private val activity: Activity) : UriHandler {
override fun openUri(uri: String) {
activity.openUrlInExternalApp(uri)
}
}

View file

@ -1,25 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.oidc
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.x.R
@ContributesBinding(AppScope::class)
class DefaultOidcRedirectUrlProvider(
private val stringProvider: StringProvider,
) : OidcRedirectUrlProvider {
override fun provide() = buildString {
append(stringProvider.getString(R.string.login_redirect_scheme))
append(":/")
}
}

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#00000000" />
</shape>

View file

@ -1,7 +0,0 @@
# Copyright (c) 2025 Element Creations Ltd.
# Copyright 2024 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
# Please see LICENSE files in the repository root for full details.
unqualifiedResLocale=en

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_bg_dark</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/transparent</item>
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
</style>
<style name="Theme.ElementX" parent="Theme.Material3.Dark.NoActionBar" />
</resources>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<!-- Must be equal to DarkColorTokens.colorThemeBg -->
<color name="splashscreen_bg_dark">#FF101317</color>
<!-- Must be equal to LightColorTokens.colorThemeBg -->
<color name="splashscreen_bg_light">#FFFFFFFF</color>
</resources>

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_bg_light</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/transparent</item>
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
</style>
<style name="Theme.ElementX" parent="Theme.Material3.Light.NoActionBar">
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
</style>
</resources>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2025 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<automotiveApp>
<uses name="notification" />
</automotiveApp>

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<!--
All backup is disabled since it would clash with encryption.
-->
<full-backup-content>
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="."/>
</full-backup-content>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<!--
All backup is disabled since it would clash with encryption.
-->
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" path="."/>
<exclude domain="file" path="."/>
<exclude domain="database" path="."/>
<exclude domain="sharedpref" path="."/>
<exclude domain="external" path="."/>
</cloud-backup>
<device-transfer>
<exclude domain="root" path="."/>
<exclude domain="file" path="."/>
<exclude domain="database" path="."/>
<exclude domain="sharedpref" path="."/>
<exclude domain="external" path="."/>
</device-transfer>
</data-extraction-rules>

Some files were not shown because too many files have changed in this diff Show more